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
, andMethod3
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
, andMethod3
) 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
Method Definitions:
Method1
,Method2
, andMethod3
are simple methods that print out "Test1", "Test2", and "Test3" respectively.Creating Threads:
Thread T1 = new Thread(Method1);
: This creates a new thread that will executeMethod1
.
Thread T2 = new Thread(Method2);
: This creates a new thread that will executeMethod2
.
Thread T3 = new Thread(Method3);
: This creates a new thread that will executeMethod3
.Starting Threads:
T1.Start();
: This starts the execution ofT1
, which will callMethod1
.
T2.Start();
: This starts the execution ofT2
, which will callMethod2
.
T3.Start();
: This starts the execution ofT3
, which will callMethod3
.Stopping the Thread:
- If you want to stop or terminate a thread, use the
Abort()
method. For example, to terminate theT1
thread, useT1.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
Thread(ThreadStart start):
- Initializes a new instance of the
Thread
class, specifying a delegate that represents the method to be executed.
- Initializes a new instance of the
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.
- Initializes a new instance of the
Thread(ThreadStart start, int maxStackSize):
- Initializes a new instance of the
Thread
class with a specified stack size.
- Initializes a new instance of the
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.
- Initializes a new instance of the
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:
Define a delegate (in this case, it's already defined as
ThreadStart
).Instantiate the delegate (binding the method to the delegate).
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 theThreadStart
delegate and binds it toMethod1
. 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 toMethod1
. We then pass this delegate instance to theThread
constructor.
Clarification:
Thread t = new Thread(Method1);
This line implicitly passes the methodMethod1
as a parameter to the constructor of theThread
class, which expects aThreadStart
delegate. The Common Language Runtime (CLR) and framework handle the creation of theThreadStart
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 aThreadStart
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);
andThreadStart 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:
Directly Pass the Method Name: This directly assigns the method
Method1
to theThreadStart
delegate. The delegate automatically binds to the method that matches its signature.Using an Anonymous Method: This uses an anonymous method to bind the
ThreadStart
delegate to the logic ofMethod1
. It allows you to write the method logic directly within the delegate if needed.Using a Lambda Expression: This uses a lambda expression to bind the
ThreadStart
delegate toMethod1
. 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
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.Example Usage:
The
Method2
method takes anobject
parameter and converts it to anint
.The
Thread
class is instantiated with theParameterizedThreadStart
delegate.The
Start
method of the thread is used to pass the parameter.Type Safety:
Since
ParameterizedThreadStart
takes anobject
, 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.Using
Join
to Wait for Thread Completion: To ensure that the main thread waits for the other threads to complete before exiting, we use theJoin
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
, andt3
) have completed their execution. Generally, we use theJoin
method in the main method.Join
Method with Timeout: TheJoin
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, sincet2
andt3
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 onlockObj
, 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:
Lowest
BelowNormal
Normal
AboveNormal
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:
Non-Generic Collections
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
, andHashtable
.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
, andHashtable
, all defined in theSystem.Collections
namespace.Q. Diffrence bitween Array and ArrayList?
Ans:-
Array ArrayList Fixed Length Variable Length Not possible to insert items We can insert item into the middle Not posible to delete items We 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 writeConsole.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, aHashtable
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 orArrayList
.
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
andHashtable
. 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>
, andStack<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);
:
Lack of Type Safety: The
Compare
method that takesobject
parameters can compare any types without type checking, leading to potential errors at runtime.Performance Issues: When we pass a value which is a
value type
(float and double) and theCompare
method takes anobject
, which is areference 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:
Generic Method Definition:
public bool Compare<T>(T a, T b) { return a.Equals(b); }
- The method
Compare
is defined with a type parameterT
. This means the method can accept arguments of any type, as long as both arguments are of the same type.
- The method
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 withfloat
as the type parameter. This ensures thatT
is set tofloat
.
- Here, the
Additional Examples with Different Types:
Compare
int
values:result =
obj.Compare
<int>(10, 10);
Compare
string
values:result =
obj.Compare
<string>("Hello", "Hello");
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 adouble
. The compiler will not allow this because it expects both arguments to be of the same type.
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 toobject
) 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 havevar
, which is similar todynamic
, but withvar
, the data type is identified at compile time (var
identifies the data type at compile time, whiledynamic
identifies it at runtime). At runtime, if you pass anint
value toa
andb
, thend1
andd2
becomeint
. If you pass afloat
value, thend1
andd2
becomefloat
. If you pass adecimal
value, they automatically convert todecimal
. Automatic conversion happens.Understanding
var
anddynamic
:-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 fromvar
, introduced in C# 3.0, which determines the type at compile time.Key Differences Between
var
anddynamic
:
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
interfaceIComparer
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:
Sort() without parameters: This uses the default way of sorting provided in the
Customer
class, which sorts byCustId
because it implements theIComparable
interface.Sort(IComparer<T>) with a custom comparer: This uses a custom way of sorting, like sorting by
Balance
, provided by anIComparer
implementation. In our example, we use theBalanceComparer
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:
Sort() without parameters:
- Uses the default way of sorting provided in the class, such as
Customer
, which sorts byCustId
because it implements theIComparable
interface. And this is also suitable for simple types likeint
,float
,double
, etc. Alwarady seen.
- Uses the default way of sorting provided in the class, such as
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.
- Takes a
Sort(IComparer<T> comparer):
- Uses can short custom way, like sorting by
Balance
, provided by anIComparer<T>
implementation. In our example, we use theBalanceComparer
class for this.IComparer
is a parameter. Alwarady seen.
- Uses can short custom way, like sorting by
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 theCompareByName
method to theSort()
method. Since theCompareByName
method matches theComparison<Customer>
delegate signature, theSort()
method internally creates a delegate for you.cus.Sort(compDele)
: Explicitly uses aComparison<Customer>
delegate (compDele
) that was previously created using theCompareByName
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:
Comparison:
- Lexicographical Order: Strings are compared character by character based on their Unicode values (similar to alphabetical order).
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 anIEnumerator
.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
andDictionary
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 inIEnumerable
enables theforeach
loop.Note's:
When you use a
foreach
loop, theforeach
loop internally calls theGetEnumerator
method to get anenumerator
. Andenumerator
is an object that enables iteration over a collection.enumerator
provides methods likeMoveNext
andReset
, and a property calledCurrent
. Enumerators come from collections that implement theIEnumerable
orIEnumerable<T>
interface (Enumerators are used with collections that haveIEnumerable
orIEnumerable<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 Method
AddEmp
:The method
AddEmp
in theOrginization
class is designed to add anEmployee
object to theEmps
list, which holds all theEmployee
objects.
public void AddEmp(Employee emp) { Emps.Add(emp); }
(Employee emp)
: The method takes one parameter, which is anEmployee
object namedemp
.
Emps.Add(emp);
: This line adds theEmployee
object passed as a parameter (emp
) to theEmps
list.Emps
is a list ofEmployee
objects, declared asList<Employee> Emps = new List<Employee>();
in theOrginization
class. TheAdd
method of theList<T>
class appends the specified element to the end of the list.Purpose: The
AddEmp
method provides a way to add newEmployee
objects to theOrginization
's internal list (Emps
). By encapsulating theAdd
functionality within this method,Explain List
List<Employee> Emps = new List<Employee>();
:
List<T>
: This is a generic collection class in theSystem.Collections.Generic
namespace. In this case,T
is replaced withEmployee
, soList<Employee>
is a list that holdsEmployee
objects.
List<Employee> Emps
:The variableEmps
is declared to hold a reference to aList<Employee>
object.
new List<Employee>()
creates a new, empty list ofEmployee
objects. At this point,Emps
is an empty list, ready to haveEmployee
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 theList<T>
class not the constructor of theEmployee
class, which initializes a new instance of the list.The
Employee
class constructor is not called innew List<Employee>()
statement. TheEmployee
class constructor will only be called when you create instances of theEmployee
class itself, such as when you add newEmployee
objects to the list.new List<Employee>()
is the constructor of theList<T>
class that is being called.The
Employee
class would be a user-defined class. Each instance of theEmployee
class represents a single employee with properties such asCustId
,Name
,City
, andBalance
.
new List<Employee>()
initializes a new list, and eachnew Employee { ... }
initializes new instances of theEmployee
class. TheEmployee
class constructor is called when creating newEmployee
objects, not when initializing the list.When you add
Employee
instances to this list, theEmployee
class constructor is called for each newEmployee
object. For example:Emps.Add(new Employee { CustId = 101, Name = "John Doe", City = "New York", Balance = 1000.0 });
In this linenew Employee { CustId = 101, Name = "John Doe", City = "New York", Balance = 1000.0 }
calls theEmployee
class constructor to create a new instance ofEmployee
and initialize it with the provided properties.
WithIEnumerable
Implementation:
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 ofEmployee
objects for employees by implementing theIEnumerable
interface. This allows it to be used in aforeach
loop.IEnumerable Implementation: The
GetEnumerator()
method returns the enumerator of theEmps
list, allowing theOrginization
class to be used in aforeach
loop.
The
Orginization
objectorgEmp
is created.Several
Employee
objects are added toorgEmp
using theAddEmp
method.The
foreach
loop iterates overorgEmp
, printing the details of eachEmployee
.
The
GetEnumerator()
method is used to enable iteration over the collection. It provides the necessary functionality for theforeach
loop to iterate through the elements of theOrginization
class.Method Signature
public IEnumerator GetEnumerator()
: This indicates a public method that returns anIEnumerator
object. TheIEnumerator
interface provides the basic mechanisms for iterating over a collection.I told you earlier: When you use a
foreach
loop, theforeach
loop internally calls theGetEnumerator
method to get anenumerator
. Andenumerator
is an object that enables iteration over a collection.enumerator
provides methods likeMoveNext
andReset
, and a property calledCurrent
. Enumerators come from collections that implement theIEnumerable
orIEnumerable<T>
interface (Enumerators are used with collections that haveIEnumerable
orIEnumerable<T>
.).
return Emps.GetEnumerator();
:Emps
is aList<Employee>
. TheList<T>
class implementsIEnumerable<T>
, so it has aGetEnumerator
method that returns an enumerator. We also usedEmps.GetEnumerator();
. TheGetEnumerator
method of theList<T>
class is called, which returns an enumerator object that can iterate through the list of employees.In
foreach
loop callsorgEmp.GetEnumerator()
to get an enumerator. The enumerator is then used to iterate over eachEmployee
object in theEmps
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 of
OrginizationEnumerator
class: TheOrginizationEnumerator
class is an implementation of theIEnumerator
interface. It is designed to iterate over a collection ofEmployee
objects contained in anOrginization
class.Fields:
orgColl
: Holds a reference to theOrginization
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 currentEmployee
object during iteration.Constructor:
public OrginizationEnumerator(Orginization org) { orgColl = org; currentIndex = -1; }
Initializes
orgColl
with the givenOrginization
object.Index start from
0
. SetscurrentIndex
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;
:IfcurrentIndex
is equal to or more than the total number of elements, it returnsfalse
, meaning the end of the collection.
currentEmployee = orgColl[currentIndex];
: it updatescurrentEmployee
with the new element and returnstrue
.Property:
Current
:public object Current => currentEmployee;
Returns the currentEmployee
object. It is typed asobject
to match theIEnumerator
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 Need
MoveNext()
,Reset()
, andCurrent
:
MoveNext()
,Reset()
, andCurrent
are part of theIEnumerator
interface, which is used for iterating over a collection.
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 returnstrue
. If it’s out of bounds (i.e., past the last element), it returnsfalse
and the iteration ends.Why We Need It: This method is essential for advancing through the collection. The
foreach
loop callsMoveNext()
to progress to the next item.
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.
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 of
GetEnumerator
Method: Allows theOrginization
class to be used withforeach
loops.
public IEnumerator GetEnumerator() { return new OrginizationEnumerator(this); }
Returns a new
OrginizationEnumerator
initialized with the current instance ofOrginization
. It means, whenGetEnumerator()
is called, it constructs a newOrginizationEnumerator
object. And this enumerator is initialized with the currentOrginization
instance, allowing it to access and iterate through theEmployee
objects in that specificOrginization
instance.The
foreach
loop callsGetEnumerator()
to get a newOrginizationEnumerator
object each time. Then uses this object to go through the items in theOrginization
collection.
new
Keyword: Thenew
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 newOrginizationEnumerator
object. This new object will be in charge of going through the items in theOrginization
collection.
this
Keyword: Thethis
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 theOrginization
class to the constructor ofOrginizationEnumerator
. This allows the enumerator to access the collection of employees in theOrginization
class.Indexing and Counting:
Indexing and Counting
are features of theOrginization
class that enhance its functionality:
Count
Property:public int Count => Emps.Count;
Purpose: Provides the number of
Employee
items in theOrginization
.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 theOrginization
collection using an index, similar to array indexing.What It Does: Retrieves the
Employee
at the specified index from theEmps
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 specifiedindex
from the internalEmps
list.Use
Employee emp = orgEmp[0];
:here's what happens:
orgEmp[0]
: The indexer is called with the index0
.get { return Emps[0]; }: The getter of the indexer is executed, which returns
Emps[0]
.Emps[0]: This accesses the
Emps
list (which is aList<Employee>
) and retrieves theEmployee
object at index0
.
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:
Employee
classOrganization
class: This class works like a collection. We define theAddEmp
method to add employees, theCount
property to return the number of items, the indexer to return an item by index, and theGetEnumerator
method to enable the foreach loop. ThisGetEnumerator
method returns anIEnumerator
type, which is an interface. Next, we define anOrganizationEnumerator
class to implement theIEnumerator
interface.OrganizationEnumerator
class: This class inherits fromIEnumerator
. I define a constructor to access theOrganization
, and then I implement theCurrent
property and two methods,MoveNext
andReset
, although we do not useReset
.IEnumerableTest
class: This is the main class. Here, we use the main method to create an instance of theOrganization
class calledorgEmp
. Then, we add employees and use a foreach loop to get the data.
Q. What is
IEnumerable
,IEnumerator
andGetEnumerator
?Ans:-
IEnumerable
:IEnumerable
is an interface used by all collection classes. It includes a method calledGetEnumerator
. Because ofGetEnumerator
, we can use a foreach loop on the collection. If we want our classes to act like collections, we need to implement theGetEnumerator
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 fromIEnumerable
that returns anIEnumerator
. 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:
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 ofvar
is determined by the value it holds.
num1
is used to capture the array data fromfrom 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:
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:
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.
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.
- To work with LINQ to SQL, you need to add a reference to the
Configuring the Connection String:
- You need to write the connection string in the configuration file (typically
App.config
orWeb.config
) of your project. This connection string will provide the necessary information for your application to connect to the SQL Server database.
- You need to write the connection string in the configuration file (typically
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 toSolution Explorer
>References
and checkSystem.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:
Open
Visual Studio Installer
.Ensure that
ASP.NET and web development
is checked under theWorkloads
tab.Go to the
Individual components
tab, search forLINQ to SQL tools
, and check it.Click
Modify
to install the required components.After installation, return to Visual Studio, select
New Item
again, and search forLINQ to SQL Classes
.When you create a
.dbml
file, it generates two additional files:
<filename>.dbml.layout
<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 fromSystem.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 fromApp.config
and establishes the connection to the database.The two panels in the OR Designer in Visual Studio serve specific purposes:
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.
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:
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 theDiagram Panel
in the OR Designer.
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>
Generated Code in
designer.cs
:The
DataClasses1.dbml
file generates adesigner.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 theDataClasses1DataContext
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 theEmployee
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.
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
inForm1.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:
Open SSMS:
Create SQL Server
Notes: Make sure to check the
Trust server certificate
option, otherwise you will get an error.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
Create
Windows Form App
:
Give the project name
ShowEmployeeFromDatabase
.
Open
Server Explorer
and configure the database:
Copy the
server name
from SSMS, then go toServer Explorer
and right-clickData Connections
. Select the data source and press next. Enter the server name, then select the database fromSelect 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.
ORM setup:
Add a new item
LINQ to SQL
(.dbml file).Go to
Server Explorer
, open the Tables folder, and drag and drop theEmployee
table to the left side ofCompany.dbml
. ORM setup is complete.Crete design:
Go to
Solution Explorer
and open Form1.cs, which is automatically created, or you can choose your own. Next, openToolbox
and thenCommon Control
to create a design like this.
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.
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
buttonInsertData
, theUpdate
buttonbutton2
, theDelete
buttonbutton4
, and theClose
buttonbutton3
. Name theDataGridView
asdataGridView1
.Create a
Form2Insert.cs
file and design it to insert data:In this form, name the 7
textBox
controls fromtextBox1
totextBox7
. Name theSubmit
buttonbutton1
, theClear
buttonbutton2
, and theClose
buttonbutton3
.Create a
Form3Update.cs
file and design it to update data:In this form, name the 7
textBox
controls fromtextBox1
totextBox7
. Name theSubmit
buttonbutton1
, theClear
buttonbutton2
, and theClose
buttonbutton3
. Set theModifiers
property of alltextBox
controls tointernal
.
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 onForm1
. 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
Connecting to the Database:
dataContext = new DatabaseDataDataContext();
This line initializes a new instance of
DatabaseDataDataContext
, which represents the database connection and provides access to theEmployees
table.
Binding Data to the DataGridView:
dataGridView1.DataSource = dataContext.Employees;
This line sets the
DataSource
property ofdataGridView1
(the grid displaying data) to theEmployees
table. This binds the employee data to the grid, displaying it on the form.
dataContext
is a variable of typeDatabaseDataDataContext
, which is a class. It is declared globally in theForm1
class asDatabaseDataDataContext dataContext;
. AnddataContext
inslized byLoadData()
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, aControl
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 aTextBox
.is TextBox
, theis
keyword checks if an object is of a specific type. Here, it checks whetherctrl
is aTextBox
.TextBox tb = ctrl as TextBox;
: This line tries to convert thectrl
object into aTextBox
. Theas
keyword is used for safe casting. Ifctrl
is aTextBox
, it will be converted to aTextBox
and assigned to the variabletb
. If not,tb
will benull
.tb.Clear();
: This line clears the text inside theTextBox
.Clear()
: TheClear()
method is aTextBox
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 selectedEmployeeID
using LINQ.SingleOrDefault
is used to retrieve a single employee record that matches the condition. If no match is found,obj
will benull
.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
Creating an Instance of
Form2Insert
:Form2Insert fi = new Form2Insert();
: This line creates an instance of theForm2Insert
class, which is the form where users can enter new employee data.
Opening
Form2Insert
:fi.ShowDialog();
: This opens theForm2Insert
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 untilForm2Insert
is closed.
Reloading Data on
Form1
:LoadData();
: After theForm2Insert
form is closed, control returns to this method. TheLoadData()
method is then called to refresh the data grid onForm1
, reflecting any new data inserted viaForm2Insert
.
Submit
button Functionality inForm2Insert.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 theDatabaseDataDataContext
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);
: TheInsertOnSubmit
method is called on theEmployees
table (which is a part of thedb
context). This method marks theeobj
object (the new employee) for insertion into the database when the changes are submitted.Save Changes
db.SubmitChanges();
: TheSubmitChanges
method is called to save all the changes made to the database through thedb
context. This commits the insertion of the newEmployee
record into theEmployees
table in the database.
Control return back to
Form1.cs
->button1_Click
.Submit
button Functionality inForm1.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 theForm3Update
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
) inForm3Update
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
) inForm3Update
is disabled, which might be an additional button not used for the update process.button1.Text = "Update";
: The text on the first button (button1
) inForm3Update
is changed to "Update" to reflect the action that will be performed when clicked.fu.ShowDialog();
: This displays theForm3Update
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, theLoadData()
method is called to refresh theDataGridView
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:
Right-click on the Stored Procedures folder under your database in the Object Explorer.
Select New Stored Procedure.
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
andBEGIN ... END
: These keywords define the body of the stored procedure. The first version of this code omitsBEGIN ... 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 theEmployee
table and orders the results byEmployeeID
.
After writing the SQL code, right-click on the
dbo.Procedure.sql
page, then select Execute or pressCtrl + 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:
Double-click on the
.dbml
file in your project.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 theDatabaseDataDataContext
class, which is defined in theDatabaseData.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 anISingleResult<Employee_SelectResult>
, whereEmployee_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.ISingleResult<T>
:ISingleResult
is an interface in LINQ to SQL, specifically in theSystem.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 ofEmployee_SelectResult
in the database, similar toTable<Employee>
, which is just a table of Employee.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 classEmployee_SelectResult
contains properties that match the columns returned by theEmployee_Select
stored procedure. For example, if the stored procedure returns columns likeEmployeeID
,FirstName
,LastName
, etc., theEmployee_SelectResult
class will have properties corresponding to these columns.
ISingleResult<Employee_SelectResult>
represents a collection ofEmployee_SelectResult
objects, where each object corresponds to a row returned by theEmployee_Select
stored procedure.ISingleResult
ensures that you can enumerate over the result set, typically using aforeach
loop, to access eachEmployee_SelectResult
object in the collection.
Difference Between Employee
Class and Employee_SelectResult
Class:
Employee
Class: Represents the entireEmployee
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 theEmployee_Select
stored procedure. This class is similar to theEmployee
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 theEmployee
table, offering more control over the data you work with.In summary, while the
Employee
class automatically includes all columns from theEmployee
table, theEmployee_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 aDataGridView
. Double-click the form (not theDataGridView
). When you do this, theForm1SQL.cs
page will open. Then, import theusing
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 indc.Employee_Select()
, we use a stored procedure to perform this.Update
Program.cs
to RunForm1SQL
: Modify theMain
Method inProgram.cs
. Change theApplication.Run
line to useForm1SQL
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 aDataGridView
and implemented theForm1SQL_Load
event to load data.Loading Data: In
Form1SQL_Load
, you instantiatedDatabaseDataDataContext
, calledEmployee_Select()
, and set theDataSource
ofdataGridView1
to the result.Running the Form: You updated
Program.cs
to runForm1SQL
instead of the default form.
This setup ensures that when
Form1SQL
is loaded, it will call theEmployee_Select()
stored procedure, retrieve the data, and display it in theDataGridView
.
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:
EmployeeID FirstName LastName JobTitle Salary Department HireDate 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 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');
DepartmentID DepartmentName Manager 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 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 theForm2SQL.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 runForm2SQL()
.
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:
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; }
Get
EmployeeID, FirstName, LastName, JobTitle and Salary
of allIT
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(); }
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
): TheDatabaseDataDataContext
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, theDatabaseDataDataContext
object is initializeddb = new DatabaseDataDataContext();
, establishing a connection to the database .Initial Data Load (
dataGridView1.DataSource = from E in db.Employees select E;
) : This line sets theDataGridView
(dataGridView1
) data source to all rows from theEmployees
table. Theselect 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 theEmployees
table. The query returns an anonymous object with a single propertyDepartment
for each row.The
Distinct()
method ensures that each department name appears only once, eliminating duplicates.DisplayMember (
comboBox1.DisplayMember = "Department";
): TheDisplayMember
property is set to "Department", which tells theComboBox
to display just the department names, not the full anonymous object{ Department = "IT" }
. The comment notes an issue with duplicates appearing ifDistinct()
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
, theSelectedIndexChanged
event triggers.The
DataGridView
is then filtered to show only those employees whoseDepartment
matches the selected value incomboBox1
. 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.
Get data ordered by
Salary
:dataGridView1.DataSource = from E in db.Employees orderby E.Salary select E;
Get data ordered by
FirstName
in descending order:dataGridView1.DataSource = from E in db.Employees orderby E.FirstName descending select E;
Get required columns:
dataGridView1.DataSource = from E in db.Employees select new { E.EmployeeID, E.FirstName, E.Department, E.Salary};
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};
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, eachdeptGroup
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.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 haveHAVING
clauses. So, how do you achieve this? InLINQ
, when you useWHERE
clauses beforeGROUP BY
, it works like aWHERE
clause. But if you useWHERE
clauses afterGROUP BY
, it works like aHAVING
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() };
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!