Generics in C# allows us to create type-safe definitions of methods and classes without the actual need to commit to an actual data type. You have worked with generic types in the previous recipe, such as enumerated data types and, soon, lambda expression. Generic types can be extended, as shown in the previous recipe; however, there are a few caveats you should keep in mind when extending generics that we will cover in this recipe.
Refer to the GenericExtensions.cs
and Models/Users.cs
files in the ExtensionMethods.Library
project for the extension methods and class. These methods are used in the Program.cs
file in the ExtensionMethods.Console
project.
The following code shows the Serialize
and SerializeUser
extension methods that we are using:
public static class GenericExtensions { /// <summary> /// Serializes an object to json string /// </summary> /// <typeparam name="T"></typeparam> /// <param name="entity"></param> /// <returns></returns> public static string Serialize<T>(this T entity) { return JsonConvert.SerializeObject(entity); } /// <summary> /// Serializes any type inheriting from IUser to json string. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="entity"></param> /// <returns></returns> public static string SerializeUser<T>(this T entity) where T : IUser { //remove password from the object entity.Password = string.Empty; return JsonConvert.SerializeObject(entity); } }
The following code shows the use of these extension methods:
[TestClass] public class GenericExtensionTests { [TestMethod] public void SerializeObjectTest() { var obj = new User { FirstName = "Cheyenne", LastName = "Powell", Password = "test" }; string json = obj.Serialize(); Assert.IsTrue(json.Contains("test")); } [TestMethod] public void Serialize_IUser_Object_Test() { var obj = new User { FirstName = "Cheyenne", LastName = "Powell", Password = "test" }; string json = obj.SerializeUser(); //SerializeUser should remove the password Assert.IsFalse(json.Contains("test")); } }
In the first snippet, we have the Serialize
extension method on the generic type T
. The purpose of this extension is to serialize an object to a JSON string. This means that this method can be executed on any type.
The following screenshot shows that the IntelliSense treats this as an object
type:
Be careful of infinite recursions as you may notice that the method is also available to be called again.
When using this extension method, you may find that it can be called on any type, including strings, integers, and so on. This may cause unintended behaviors and could be, potentially, dangerous.
The second code snippet gives us an upgraded and safer version of this method. SerializeUser
utilizes the where
clause to place a condition on the type of generic data type this method is exposed to:
public static string SerializeUser<T>(this T entity) where T : IUser
This clause states that this method is only available to those objects that are of type IUser
, or inherits from it. In the previous screenshot, you will notice that this extension method is not available to that data type in the IntelliSense. Even though our parameter entity
is of type T
, the clause tells the compiler it is actually an IUser
type, the IntelliSense will operate accordingly.
The purpose of our SerializeUser
method is to serialize the object of the IUser
type while removing the password from it. In the following image, both methods are available to us in IntelliSense. The compiler sees that obj
is of the User
type which inherits from the IUser
type which tells the IntelliSense to show us SerializeUser
.
The following screenshot shows the IntelliSense being activated and displays the available methods for both the object
and IUser
type:
When working with generics, try to narrow down the types the method will apply to by using the where
clause unless you are certain that the method should apply to every data type available to the compiler.
In this recipe, you have have learned generics behave with extension methods with a few things to watch out for and ways to work around them.