Top 20 C# Features Every .NET Developer Must Know

Top 20 C# Features Every .NET Developer Must Know

05 Oct 2024
Advanced
3.06K Views
49 min read
Learn with an interactive course and practical hands-on labs

Free C# Foundation Course: Learn C# In 21 Days

Features of C#

C# is a widely used programming language developed by Microsoft .NET platform for building applications like desktop software, web services, and mobile apps. What makes it popular are the C# features that make coding easier, faster, and more secure. The features of C# help developers solve common coding challenges and write clean, efficient code. By understanding these C# features, you'll be able to create better programs and fully use the power of the C# language.

This C# tutorial provides complete guidance to understand the features of C#, including Generic, Partial Class, LINQ: Language Integrated Query, Dynamic Type, Async-Await in C#, and many more. By learning these features of C#, you can enhance your programming capability. If you are a beginner, this C# Developer Roadmap explains how to become a developer in the C# language.

Top 20 Features of C#

We will discuss here the top 20 essential features of C# that help you in your programming language. That is:

Top 20 Features of C#

1. Generics

The idea of type parameters is brought to .NET via generics, enabling the design of classes and methods that postpone defining one or more types until the class or function is defined and created by client code. It is commonly used to create collection classes. The .NET class library contains several generic collection classes in the System.Collections.Generic namespace. You can create your generic interfaces, classes, methods, events, and delegates.

Example

using System;

namespace GenericExample
{
    // Generic class
    public class GenericBox<T>
    {
        private T _value;

        public void SetValue(T value)
        {
            _value = value;
        }

        public T GetValue()
        {
            return _value;
        }
    }
    // Class with a generic method
    public class GenericMethodExample
    {
        public void PrintValue<T>(T value)
        {
            Console.WriteLine(value);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            // Using the generic class
            GenericBox<int> intBox = new GenericBox<int>();
            intBox.SetValue(123);
            Console.WriteLine("Value in intBox: " + intBox.GetValue()); // Output: 123
            GenericBox<string> strBox = new GenericBox<string>();
            strBox.SetValue("Hello, Generics!");
            Console.WriteLine("Value in strBox: " + strBox.GetValue()); // Output: Hello, Generics!
            // Using the generic method
            GenericMethodExample example = new GenericMethodExample();
            example.PrintValue(42); // Output: 42
            example.PrintValue("Generics in C#"); // Output: Generics in C#
        }
    }
}  

Output

 Value in intBox: 123
Value in strBox: Hello, Generics!
42
Generics in C#

2. Partial Class

This feature was introduced in C# 2.0. To split a class definition across multiple files, use the partial keyword modifier. When working with an automatically generated source, code can be added to the class without having to recreate the source file.

The partial keyword indicates that other parts of the class, structure, or interface can be defined in the namespace. All the parts must use the partial keyword. All the parts must be available at compile time to form the final type. All the parts must have the same accessibility, such as public, private, and so on.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Hello
{
	
public partial class Coords
{
    private int x;
    private int y;

    public Coords(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

public partial class Coords
{
    public void PrintCoords()
    {
        Console.WriteLine("Coords: {0},{1}", x, y);
    }
}

class TestCoords
{
    static void Main()
    {
        Coords myCoords = new Coords(10, 15);
        myCoords.PrintCoords();

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}

}

In the code above, one partial class definition contains the properties and constructors of the class Coords, while another partial class definition has the member PrintCoords.

Output

Coords: 10,15

3. LINQ: Language Integrated Query

It was introduced in the C# 3.0 version. Allows to query various data sources like C# collection, SQL, and XML-like data using common query syntax.

Example

LINQ can be used to query in-memory objects and collections. It provides a set of standard query operators that operate on collections implementing IEnumerable>


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

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

        // LINQ query to filter even numbers
        var evenNumbers = from num in numbers
                          where num % 2 == 0
                          select num;

        foreach (var evenNumber in evenNumbers)
        {
            Console.WriteLine(evenNumber);
        }
    }
}

Output

2
4

4. Lambda Expressions

A C# Lambda Expression is a brief code block that takes two arguments and outputs one. The function in question is specified as anonymous or function-name-free. In C# 3.0, it was first shown. Provides a concise way to write inline expressions or anonymous methods. They are often used with LINQ queries or as a convenient way to define delegates or event handlers.

Syntax

(parameterList) => lambda body

Here,

  • parameterList - list of input parameters
  • => - a lambda operator
  • lambda body - can be an expression or statement

Example- Simple Delegate Code Using Lambda Expression


using System;
class Program
{
    static void Main()
    {
        // delegate using lambda expression 
        Func<int, int> square = num => num * num;

        // calling square() delegate 
        Console.WriteLine(square(7));
    }
}

In the above code, we don't need to define a separate method. We have replaced the pointer to the square() method with the lambda expression.

Output

49

5. Extension Methods

In C# 3.0, this functionality was added. In the event that the class's source code is unavailable or we are not authorized to make changes to it, it enables us to enhance its functionality in the future by adding new methods to the class without altering its source code.

Example

 using System;

namespace ExtensionMethodsDemo
{
    // Step 1: Create a static class for extension methods
    public static class StringExtensions
    {
        // Step 2: Create a static method with 'this' keyword
        public static bool IsNullOrEmpty(this string str)
        {
            return string.IsNullOrEmpty(str);
        }
    }
   class Program
    {
        static void Main(string[] args)
        {
            string testString = null;
            // Using the extension method
            if (testString.IsNullOrEmpty())
            {
                Console.WriteLine("The string is null or empty.");
            }
            else
            {
                Console.WriteLine("The string has a value.");
            }
        }
    }
}  

Output

 The string is null or empty.

6. Dynamic Type

The Dynamic Type is introduced as part of C# 4 to write dynamic code in C#. It defers type checking from compile time to runtime. Method calls and property accesses are resolved at runtime, which can lead to performance overhead. It is advantageous when you want to avoid typecasting and interacting with dynamic languages like Pythonand JavaScript.


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

class Program
{
    static void Main()
    {
        dynamic dynamicVar = 10;
        Console.WriteLine(dynamicVar);

        dynamicVar = "Hello, World!";
        Console.WriteLine(dynamicVar);

        dynamicVar = new List<int> { 1, 2, 3 };
        Console.WriteLine(dynamicVar.Count);
    }
}

Output

10
Hello, World!
3

7. Async/Await

Async and Await, introduced in C# 5.0, are the code markers that mark code positions from where the control should resume after completing a task. It helps to write asynchronous code, which is essential for non-blocking UI and server applications.

Example

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("Starting the async operation...");

        // Call the async method and wait for it to complete
        await PerformLongOperation();

        Console.WriteLine("Operation completed!");
    }

    // An async method that simulates a long-running operation
    public static async Task PerformLongOperation()
    {
        Console.WriteLine("Performing a long operation...");
        
        // Simulate a delay (e.g., long calculation, file processing, etc.)
        await Task.Delay(3000); // Wait for 3 seconds
        
        Console.WriteLine("Long operation finished!");
    }
}   

Output

 Starting the async operation...
Performing a long operation...
Long operation finished!
Operation completed!

The code above calls from the Main function and does not rely on either Method1 or Method2. It is evident that there is no delay between Methods 1 and 2.

8. String Interpolation

It was first available in C# 6.0. It enables you to easily insert variables and expressions inside string literals. Compared to traditional string concatenation or the String. The format method provides a concise and more readable way to create formatted strings.

Example


using System;

class Program
{
    static void Main()
    {
        string name = "ScholarHat";
        int age = 2;
        // String interpolation using $""
        string message = $"Hello, my name is {name} and I am {age} years old.";

        Console.WriteLine(message);
    }
}

Output

Hello, my name is ScholarHat and I am 2 years old.

9. Expression-Bodied Members

Introduced in C# 6.0, expression-bodied members are a syntactic shortcut. With lambda-like syntax, they let you create succinct one-liner methods, properties, and other members. They can be used for methods, properties, indexers, and event accessors.

Example


using System;

class MyClass
{
    // Expression-bodied method
    public int Add(int x, int y) => x + y;

    static void Main()
    {
        MyClass myObject = new MyClass();
        int result = myObject.Add(5, 7);
        Console.WriteLine(result);  
    }
}

Output

12

10. Auto-Property Initializers

In C# 6.0, they were first introduced. One technique to initialize the value of an auto-implemented property right within the property declaration is to use auto-property initializers. This makes the syntax for setting a property's default value simpler.

Using auto-property initializers saves space and eliminates the need to initialize properties in the constructor. They are a convenient approach to provide default values for properties.

Example


 using System;

public class MyClass
{
    // Auto-property initializer
    public int MyProperty { get; set; } = 42;

    static void Main()
    {
        MyClass myObject = new MyClass();
        Console.WriteLine(myObject.MyProperty);  // Output: 42
    }
}

Output

42

11. Tuples and Deconstruction

Tuples allow the grouping of multiple values in a single object without creating a custom class. Deconstruction helps to unpack tuple values into separate variables.

Also Read: Tuples in Python with Examples - A Beginner Guide

Example

 using System;

class Program
{
    static void Main(string[] args)
    {
        // Creating a tuple with multiple types
        (int id, string name, double salary) employee = (1, "Ankita", 50000.75);

        // Accessing tuple elements by position
        Console.WriteLine($"ID: {employee.Item1}, Name: {employee.Item2}, Salary: {employee.Item3}");

        // Deconstructing tuple into individual variables
        (int id, string name, double salary) = employee;
        Console.WriteLine($"ID: {id}, Name: {name}, Salary: {salary}");
    }
}

Output

 ID: 1, Name: Ankita, Salary: 50000.75
ID: 1, Name: Ankita, Salary: 50000.75

12. Pattern Matching

Introduced in C# 7.0, pattern matching is a feature that lets you verify the structure or form of a value directly in code. Streamlining the syntax for frequent type checks and extractions reduces boilerplate code and improves code readability.

Example of Type Pattern

 using System;

class Program
{
    static void Main(string[] args)
    {
        object obj1 = 42;
        object obj2 = "Hello, World!";
        object obj3 = new Point(5, 10);
        object obj4 = null;

        // Type Pattern
        PrintObjectType(obj1);
        PrintObjectType(obj2);
        PrintObjectType(obj3);
        PrintObjectType(obj4);
    }

    static void PrintObjectType(object obj)
    {
        // Using the 'is' operator with pattern matching
        switch (obj)
        {
            case int i:
                Console.WriteLine($"Integer: {i}");
                break;
            case string s:
                Console.WriteLine($"String: {s}");
                break;
            case Point p when p.X > 0 && p.Y > 0:
                Console.WriteLine($"Point in the positive quadrant: ({p.X}, {p.Y})");
                break;
            case Point p:
                Console.WriteLine($"Point: ({p.X}, {p.Y})");
                break;
            case null:
                Console.WriteLine("Null object.");
                break;
            default:
                Console.WriteLine("Unknown type.");
                break;
        }
    }
}

// Sample class for demonstrating property pattern matching
public class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Output

 Integer: 42
String: Hello, World!
Point in the positive quadrant: (5, 10)
Null object.

13. Nullable Reference Types

Introduced in C# 8.0, Nullable Reference Types provide annotations to the type system that indicate whether or not a reference type can be null, enabling developers to construct more secure and reliable code. It enables you to explicitly state your intentions regarding nullability in the code, and it enables the compiler to flag possible null-reference problems with warnings.

Example

 #nullable enable // Enable nullable reference types

using System;

class Program
{
    static void Main(string[] args)
    {
        string? name = null; // Nullable reference type
        string nonNullableName = "Alice"; // Non-nullable reference type

        PrintName(name); // This may lead to a warning
        PrintName(nonNullableName);
    }

    static void PrintName(string? name)
    {
        // Use the null-coalescing operator to provide a default value
        Console.WriteLine(name ?? "Name is null");
    }
}  

Output

 Name is null
Alice

14. Default Interface Methods

Introduced in C# 8.0, the methods allow you to provide a default implementation for methods in an interface. This feature helps maintain backward compatibility when introducing new methods to an interface without requiring all implementing classes to provide an implementation. It is useful when you want to extend existing interfaces without breaking existing implementations.

Example

 using System;

public interface IShape
{
    void Draw();

    // Default method with an implementation (supported in C# 8 and above)
    void Display()
    {
        Console.WriteLine("Default Display Implementation");
    }
}

public class Circle : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }

    // Circle will use the default Display implementation from the interface
}

public class Square : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing a square");
    }

    // Custom implementation for Display in Square class
    public void Display()
    {
        Console.WriteLine("Custom Display Implementation for Square");
    }
}

class Program
{
    static void Main()
    {
        IShape circle = new Circle();
        IShape square = new Square();

        circle.Draw();
        circle.Display(); // Calls the default implementation from the interface

        square.Draw();
        square.Display(); // Calls the custom implementation in Square class
    }
}  

Output

 Drawing a circle
Default Display Implementation
Drawing a square
Custom Display Implementation for Square 

15. Record Types

The record types feature was introduced in C# 9.0 to provide a concise way to declare immutable types. Records simplify the process of creating and working with immutable classes by automatically generating common methods like Equals, GetHashCode, and ToString. They are particularly useful for modeling data transfer objects (DTOs) and other types where immutability and value equality are essential.

We can't directly run a record-type feature application program. To run them, we should follow some required steps:

1. Create a New Console Application

  • You can create a new console application to run the program using the .NET CLI (Command Line Interface).
  • Open a terminal (or command prompt) and run the following commands:
 dotnet new console -n RecordExample
cd RecordExample

2. Write the Code

Open the Program.cs file located in the RecordExample folder and replace the existing code with the following code that demonstrates the use of a record:

 using System;

public record Student(string Name, int Age);

class Program
{
    static void Main(string[] args)
    {
        var student1 = new Student("Ankita", 22);
        var student2 = new Student("Ankita", 22);
        
        Console.WriteLine($"Student 1: {student1.Name}, Age: {student1.Age}");
        Console.WriteLine($"Student 2: {student2.Name}, Age: {student2.Age}");

        // Check if the two records are equal
        Console.WriteLine($"Are the two students equal? {student1 == student2}");  // Output: True
    }
}   

3. Run the Program

In the terminal (or command prompt), navigate to the folder where your project is located and run:

 dotnet run

4. Expected Output

 Student 1: Ankita, Age: 22
Student 2: Ankita, Age: 22
Are the two students equal? True

16. Top-Level Statements

Introduced in C# 9.0, it allows you to write simpler C# programs by omitting the traditional Main method and placing the program logic directly at the top level of the file. It simplifies the structure of simple programs by reducing boilerplate code.


using System;

Console.WriteLine("Hello, ScholarHat#!");

int result = Add(5, 7);
Console.WriteLine($"Result of addition: {result}");

// A simple function using top-level statement
int Add(int a, int b) => a + b;

Output

Hello, ScholarHat#!
Result of addition: 12

17. Global Using Directives

The global using directives feature was introduced in C# 10.0. It allows you to specify a set of directives that will be applied globally to all files in a project without the need to include them explicitly in every file. This can help reduce the boilerplate code in your files and provide a more consistent and simplified coding experience.

We can't directly run a record-type feature application program. To run them, we should follow some required steps:

1. Create a New Console Project

First, we will create a new console application by following the commands after open the Visual Studio.

 dotnet new console -n GlobalUsingExample
cd GlobalUsingExample

2. Create a File for Global Using Directives

In the GlobalUsingExample folder, create a new file called GlobalUsings.cs (this file can have any name, but it’s typically named this way for clarity).

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

3. Write the Code

Now, open the Program.cs file and write the main logic without repeating the using System or other directives:

 class Program
{
    static void Main(string[] args)
    {
        var names = new List { "Ankita", "Amit", "Rahul" };
        
        foreach (var name in names)
        {
            Console.WriteLine(name);
        }
    }
}   

We use the 'dotnet run' command to run the program:

Expected Output

 Ankita
Amit
Rahul

18. List Patterns

List patterns in C# are a type of pattern introduced in C# 9.0 to match elements of a list or array succinctly and expressively. They provide a concise way to destructure and match the elements of a list or array.

Example

using System;
using System.Collections.Generic;

// Define a Person class
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Define an Animal class
public class Animal
{
    public string Species { get; set; }
}

// Main Program
public class Program
{
    // Method to describe an object using pattern matching
    public static string Describe(object obj)
    {
        return obj switch
        {
            int i => $"This is an integer: {i}",
            string s when s.Length > 0 => $"This is a non-empty string: {s}",
            string => "This is an empty string.",
            Person { Age: 30, Name: "John" } => "This is John, 30 years old.",
            Person p => $"This is {p.Name}, {p.Age} years old.",
            Animal { Species: "Dog" } => "This is a Dog.",
            null => "This is null.",
            _ => "Unknown type"
        };
    }

    // Main method
    public static void Main(string[] args)
    {
        // Create instances of Person and Animal
        var john = new Person { Name = "John", Age = 30 };
        var dog = new Animal { Species = "Dog" };
        
        // Test various cases
        var results = new List<object>
        {
            42,
            "Hello, world!",
            " ",
            john,
            dog,
            null,
            3.14 // Unknown type
        };

        // Describe each object
        foreach (var item in results)
        {
            Console.WriteLine(Describe(item));
        }
    }
}

Output

This is an integer: 42
This is a non-empty string: Hello, world!
This is a non-empty string:  
This is John, 30 years old.
This is a Dog.
This is null.
Unknown type 

19. 'required' Modifier

Whenever a class is declared with a property or field with the required keyword, the caller is forced to initialize in the object initializer scope. It was introduced in C# 11.

We can't directly run the required modifier program in our compiler. You have to run this on Visual Studio. You have to follow these steps:

1. Create a Console Application

First, we should create a console application by following these commands:

  dotnet new console -n RequiredModifierExample

2. Navigate to the project folder

Second, we navigate to the folder by the command:

  cd RequiredModifierExample

Write the Code

  • Open the project folder using a code editor (like Visual Studio Code or any other text editor).
  • Find the Program.cs file and replace the content with the following code that demonstrates the required modifier
 public class Student
{
    // Required properties
    public required string Name { get; init; }
    public required int Age { get; init; }

    // Optional property with a default value
    public string Course { get; set; } = "BCA";
}

class Program
{
    static void Main(string[] args)
    {
        // Object initialization with required properties
        var student = new Student
        {
            Name = "Ankita",  // Required property
            Age = 22          // Required property
            // Course is optional and defaults to "BCA"
        };

        // Output the details of the student
        Console.WriteLine($"Name: {student.Name}, Age: {student.Age}, Course: {student.Course}");
    }
}  

The possible output will be generated after building the project and running the above program.

Expected Output

 Name: Ankita, Age: 22, Course: BCA

20. Collection Expressions

A collection expression is a terse syntax that, when evaluated, can be assigned to many different collection types. It contains a sequence of elements between [ and ] brackets. It can be converted to many different collection types.

Example

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

class Program
{
    static void Main(string[] args)
    {
        // Example 1: Initializing a list with values
       List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        Console.WriteLine("Numbers List:");
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
        // Example 2: Initializing a dictionary with key-value pairs
        Dictionary<string, int> ages = new Dictionary<string, int>
        {
            { "Ankita", 25 },
            { "Rahul", 30 },
            { "Suman", 28 }
        };
        Console.WriteLine("\nAges Dictionary:");
        foreach (var entry in ages)
        {
            Console.WriteLine($"{entry.Key}: {entry.Value}");
        }
        // Example 3: Filtering even numbers using LINQ
         List<int> evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
        Console.WriteLine("\nEven Numbers List:");
        foreach (var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}  

Output

 Numbers List:
1
2
3
4
5

Ages Dictionary:
Ankita: 25
Rahul: 30
Suman: 28

Even Numbers List:
2
4 
Read More:
OOPs Interview Questions and Answers in C#
Top 50 C# Interview Questions and Answers To Get Hired
Conclusion

In conclusion, we have covered the Top 20 Features of C#C# supports you with powerful features that make it a versatile and efficient programming language. Each C# feature helps you a lot in your programming language learning. Whether you’re working on small projects or large-scale applications, mastering these top 20 features in C# will help you write cleaner, more efficient, and maintainable code. To master C# programming language, Scholarhat provides you with the Full-Stack .NET Developer Certification Training Course and .NET Solution Architect Certification Training for better understanding.

FAQs

Records are reference types that provide an easy way to define immutable data models with built-in functionality like value equality comparison, which compares the values of properties instead of their references. 

The var keyword allows for implicit typing, which means the compiler determines the type of the variable based on the value assigned to it. This simplifies code without sacrificing type safety. 

Auto-property initializers provide a way to initialize the value of an auto-implemented property directly within the property declaration, simplifying the syntax for setting default values for properties.

The null-conditional operator (?.) allows you to check for null values before accessing members of an object. This prevents NullReferenceException errors and makes the code safer and easier to read. 

Take our Csharp skill challenge to evaluate yourself!

In less than 5 minutes, with our skill challenge, you can identify your knowledge gaps and strengths in a given skill.

GET FREE CHALLENGE

Share Article
About Author
Shailendra Chauhan (Microsoft MVP, Founder & CEO at Scholarhat by DotNetTricks)

Shailendra Chauhan, Founder and CEO of ScholarHat by DotNetTricks, is a renowned expert in System Design, Software Architecture, Azure Cloud, .NET, Angular, React, Node.js, Microservices, DevOps, and Cross-Platform Mobile App Development. His skill set extends into emerging fields like Data Science, Python, Azure AI/ML, and Generative AI, making him a well-rounded expert who bridges traditional development frameworks with cutting-edge advancements. Recognized as a Microsoft Most Valuable Professional (MVP) for an impressive 9 consecutive years (2016–2024), he has consistently demonstrated excellence in delivering impactful solutions and inspiring learners.

Shailendra’s unique, hands-on training programs and bestselling books have empowered thousands of professionals to excel in their careers and crack tough interviews. A visionary leader, he continues to revolutionize technology education with his innovative approach.
Accept cookies & close this