Before we jump into creating an extension method, could someone please tell me what on earth extension methods are? Well, how about we start with a nice little introduction.
Introduction
Well as per MSDN, Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are static methods, but they're called as if they were instance methods on the extended type.
You know what? That sounds good to hear, Microsoft! let's make a sense of it.
Extension methods are a perfect example of Open-Closed Principle, which says, "Open for Extension, Closed for Modification". Software entities should be open for extension but closed for modification. This simply means that an entity should be easily extendable without modifying itself.
- What it's saying is, now you can explicitly add a new method to existing "class String", Isn't that something?
- If you want to check if the string is palindrome or not. you can simply create an extension method and be done with it once and for all.
But the question is HOW??
And for everyone's sake, we are not actually going to do string palindromes like 2 year olds. We are going to implement something useful. Let's code an extension method to get the description of the enum by its value. Believe me, I had to google this answer so many times that I decided to write this article so I don't have to swim across the pacific ocean full of plastic waste just to get my tuna.
Implementation
- First things first, Can you make changes to class String?
- The answer is, Big Fat NO, of course not! you can not make changes to any of the system's classes for that matter, Now you'd argue but I can inherit the "class String" and extend additional functionality by overriding its existing methods. Ha-Ha-Ha, you can't do that either because it's a damn sealed class means it is closed for inheritance as well.
So how do we extend the behavior of existing classes?
Have a sly look at figure 1, This is the big picture of what we are going to do.
Figure 1: Extension methods for type enum and string respectively
Figure 1 explanation: so as you can see there are 2 extension methods,
- First one, GetEnumDescription() is for "type enum" to fetch "description" of an enum values.
- Second, extension method GetEnumValueByDescription() which is for "type string" to fetch the "enum value" by their description.
Go ahead and create an enum as shown in listing 1, This is a enum of flagship phones.
public enum FlagshipSmartphone {
[Description("iPhone 13 Pro Max")]
Apple,
[Description("Samsung Galaxy Note 20")]
Samsung,
[Description("OnePlus 9 Pro")]
OnePlus,
[Description("Google Pixel 6 Pro")]
Google
}
Listing 1: FlagshipSmartphone.cs
GetEnumDescription()
In order to get the description, we are going to create an extension method on enum.
There are a few rules we need to take care of while creating an extension method.
Rule #1
Extension methods are defined as static but are called by using instance method syntax. In the following syntax we have created a static method GetEnumDescription().
public static string GetEnumDescription()
{
}
Listing 2: Method GetEnumDescription's empty body
Rule #2
The first parameter specifies which type for which we are creating the extension method, in this example that would be "Enum". The parameter is always decorated by "this modifier".
public static string GetEnumDescription(this Enum enumValue)
{
}
Listing 3: GetEnumDescription() with this parameter
Rule #3
In order to access extension methods, one explicitly has to import the namespace. In order to access the following "GetEnumDescription() method" we need to add "using ExtensionMethod;" namespace in caller class.
namespace ExtensionMethod
{
public static class Extension
{
public static string GetEnumDescription(this Enum enumValue)
{
}
}
}
Listing 3: GetEnumDescription() with it's namespace
Rule #4
"Overriding is strictly prohibited", you can use extension methods to extend the behavior of class or interface, but not to override the existing behavior.
Now that we are done with all the rules, let's add some logic to this empty body. In following listing all I am doing is fetching the "fieldinfo" of "type enum" in line number 3 and in next line I am searching for the custom attribute which is "type of DescriptionAttribute" if type matched then simply return the description, if "type is not DescriptionAttribute" then simply throw an exception.
public static string GetEnumDescription(this Enum enumValue)
{
var field = enumValue.GetType().GetField(enumValue.ToString());
if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
{
return attribute.Description;
}
throw new ArgumentException("Item not found.", nameof(enumValue));
}
Listing 4: GetEnumDescription()
All set, let's see the magic. As per our diagram, let's call this extension method in the "class Program", In the following listing 5, we are creating a variable of type "FlagshipSmartphone enum" with value of "Samsung". Then in line number 4 we are actually calling our newly created extension method with the help of (.) dot operator.
using ExtensionMethod;
FlagshipSmartphone Samsung = FlagshipSmartphone.Samsung;
string Description = Samsung.GetEnumDescription();
Console.WriteLine(Description);
Listing 5: Calling extension method()
Run the application and you should be able to see output as figure 2.
Figure 2: Output of GetEnumDescription()
GetEnumValueByDescription()
Now that we are familiar with all the bits and pieces let's jump into code. We are going to reuse "GetEnumDescription()" to avoid rewriting the same logic.
- You may ask, what are we doing in listing 6, Great question!!!
- First we are using the "first parameter string as "this" parameter".
- Then we are going to return generic type parameter "<T>" to make this method reusable across all the types of enums.
- As far as logic is concerned, we are looping through enums values looking for matching description. Once description matches we are simply returning that enum value if there is no enum with a matching description then we are simply throwing an exception. You can handle this the way you want but for simplicity, right now I am just throwing an exception.
public static T GetEnumValueByDescription<T>(this string description) where T : Enum
{
foreach (Enum enumItem in Enum.GetValues(typeof(T)))
{
if (enumItem.GetEnumDescription() == description)
{
return (T)enumItem;
}
}
throw new ArgumentException("Not found.", nameof(description));
}
Listing 6: GetEnumValueByDescription()
Let's go ahead and call this method in our "class Program". As you can see at line number 8. we are using a (.) dot operator on a string variable iphone to call our extension method.
using ExtensionMethod;
FlagshipSmartphone Samsung = FlagshipSmartphone.Samsung;
string Description = Samsung.GetEnumDescription();
Console.WriteLine(Description);
string iphone = "iPhone 13 Pro Max";
FlagshipSmartphone flagship = iphone.GetEnumValueByDescription<FlagshipSmartphone>();
Console.WriteLine(flagship);
Listing 7: Calling GetEnumValueByDescription() at line number 8.
Let's run this guy here! and you should be able to see an output as figure 3.
Figure 3: Output of GetEnumValueByDescription()
This is how you can extend the behavior of existing classes without modifying them. There are other design patterns as well you can use to achieve this but this is a shortcut you could use.
There is one more interesting thing. As it is a user defined static class you can also call this method directly using class name.
string iphone = "iPhone 13 Pro Max";
FlagshipSmartphone flagship = Extension.GetEnumValueByDescription<FlagshipSmartphone>(iphone);
Listing 8: Calling extension method with static class
There are some predefined extension methods available in C#. If you take following example. Here we have an "int array" and we are using "FirstOrDefault()" on array, But guess what? Array doesn't have any method named FirstOrDefault(). This is happening because FirstOrDefault() is an extension method which can be used by any collection which is implementing "IEnumerable interface", Here Array does implement IEnumerable interface, Refer figure 4.
int[] inputArray = { -1, 5, 10, 25, 9, 48, 67};
int numberTen = inputArray.FirstOrDefault(x=> x == 10);
Listing 9: Using extension method FirstOrDefault()
Figure 4: class Array
As we learn in listing 8, that we can directly use static class to call the extension method, we can also rewrite code in listing 9 as below.
Note: The first parameter is, "this parameter of type collection", which means you will have to pass an "integer array" as first parameter to the FirstOrDefault() method.
int[] inputArray = { -1, 5, 10, 25, 9, 48, 67};
int numberTen = Enumerable.FirstOrDefault(inputArray, x => x == 10);
Listing 10: Calling extension method with static class
But it always preferred to use extension methods, I mean that's why they are designed for.
Conclusion
This one topic of extension method gives us a broader picture of how classes can be extended for additional behavior without modifying it. This also gives us an idea how static classes work. You can dig into the system's classes to discover extension methods. You'll be surprised to know how many extension methods you've been using so far.