다음을 통해 공유


Extension methods (C#)

Introduction

This article will discuss the fundamentals to extend existing types without creating new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type. The following MSDN page lays out the basics for creating language extension methods.

For client code written in C#, F# and Visual Basic, there is no apparent difference between calling an extension method and the methods that are defined in a type. The primary language will be C# but extension methods as mention prior will work in other languages and can be written in one language and used in another language.

Four advantages of extension methods

  1. Discoverability – variable name plus dot will give you the method name via Intellisense, this serves you time when coding.
  2. Cleaner and simpler syntax – all you must do is write variable name, dot and the extension method name and you are done.
  3. Code readability – based on the above example, use of extension methods results in few lines of code written. This greatly improves the code readability.
  4. Extend functionality of libraries you do not have access to the code – suppose you have a third-party library and you would like it to add more methods without recompiling the original code, you can take advantage of extension methods and add the functionality yourself. Your development team will use them as if they were shipped with the original library.

Example of a language extension for formatting a phone number which is can be useful

public static  string FormatPhoneNumber(this string  sender)
{
    string result = string.Empty;
 
    if (sender.Contains("(") == false  && sender.Contains(")") == false  && sender.Contains("-") == false)
    {
        if (sender.Length == 7)
        {
            result = sender.Substring(0, 3) + "-" + sender.Substring(3, 4);
        }
        else if  (sender.Length == 10)
        {
            result = $"({sender.Substring(0, 3)}) {sender.Substring(3, 3)}-{sender.Substring(6, 4)}";
        }
        else if  (sender.Length > 10)
        {
 
            result = $"({sender.Substring(0, 3)}) {sender.Substring(3, 3)}-{sender.Substring(6, 4)} x{sender.Substring(10)}";
        }
    }
    else
    {
        result = sender;
    }
 
    return result;
}

Language extension methods written in C# can be used in Visual Basic.

Dim phoneNumber As String  = "9991231234"
Console.WriteLine($"Formatted phone number: {phoneNumber.FormatPhoneNumber()}")

C#

var phoneNumber = "9991231234";
Console.WriteLine($"Formatted phone number: {phoneNumber.FormatPhoneNumber()}");

Examples for converting a string to Integer

/// <summary>
/// Convert string value to int, if string is not a valid int return 0.
/// </summary>
/// <param name="sender">String to convert to Int32</param>
/// <returns>An int</returns>
public static  int ToInt32(this string  sender)
{
    return sender.IsNullOrWhiteSpace() || (sender == null) || sender.IsNumeric() == false  ? Convert.ToInt32(0) : Convert.ToInt32(sender);
}
/// <summary>
/// Convert string value to int
/// </summary>
/// <param name="sender">String to convert to Int32</param>
/// <param name="pThrowExceptionOnFailure">When set to true will throw a runtime
/// exception while set to false will not throw a runtime exception</param>
/// <returns>Int</returns>
/// <remarks>
/// An exception is thrown if conversion fails with pThrowExceptionIfFailed = true.
/// </remarks> 
public static  int ToInt32(this string  sender, bool  pThrowExceptionOnFailure = false) 
{
    var valid = int.TryParse(sender, out  var result);
 
    if (valid) return result;
    if (pThrowExceptionOnFailure)
    {
        throw new  FormatException($"'{sender}' cannot be converted as int");
    }
 
    return result;
}

 

When should you use extension methods?

The first reason is when a developer is frequently writing custom functions that are either used often or how to write a function is not remembered. 

For example, a simple example would be to create a string delimited by a specific type such as a list of integers which for many is simple but not for every developer. 

A more complex example, a developer has written a SQL statement with parameters (no matter the data provider) in code and has a syntax error or the query functions without errors but returns unexpected results. Code to recreate the SQL statement with values for the parameters rather than place holders is quite a bit of code and not needed often. There could be a language extension (and there is one in the sample source along with this article) that works off the command object to create a string representation of the query with values which in turn can be pasted into a database editor such as SSMS (SQL-Management Studio), Toad for Oracle or directly into a MS-Access database new query.

Other reasons, extending a component or control along with taking a .NET Framework function and creating a method-based version.

Defining an extension method

  1. Define a static class .eg. public static class StringExtensions.
  2. The class need not be public but must be accessible to any callers (see also Access Modifiers), if unsure use public.
  3. The first parameter of the method specifies the type that the method operates on; it must be preceded with the this modifier.
  4. In the calling add, add a using directive to specify the namespace that contains your extension method.

Here is a easy yet useful example which takes string.IsNullOrWhiteSpace and by creating a language extension method is used no different than a method call.

public static  bool IsNullOrWhiteSpace(this string  sender)
{
    return IsNullOrWhiteSpace(sender);
}

There is a gotcha with the above extension method, the Framework will do recursive calls on the extension method as it sees return IsNullOrWhiteSpace as itself. To fix this we need to tell the compiler to call system.IsNullOrWhiteSpace instead.

public static  bool IsNullOrWhiteSpace(this string  sender)
{
    return string.IsNullOrWhiteSpace(sender);
}

Extension hoarders

There are some developers who will create a massive collection of language extension method seemingly to simply collect them as many really serve no real purpose, nothing or little gained. There is nothing wrong with a large meaningful set of language extension methods, just keep them to methods that will be used at some time or another.

Extension class projects

Although extension methods may be placed directly into a project this works although most developers will write many projects during their career, this should take extension methods from a given project to a Visual Studio solution with a project for the language extension methods. One extensions are required by a project simply add a reference to the project for the extension methods. Alternately the solution for extension method can contain one project per extension type, for instance a project for string extension methods, another project for generic extension methods and so forth.

Extension namespaces

*"Extension methods are brought into scope by including a using [namespace]; statement at the top of the file. You need to know which C# namespace includes the extension methods you’re looking for, but that’s easy to determine once you know what it is you’re searching for.

When the C# compiler encounters a method call on an instance of an object and doesn’t find that method defined on the referenced object class, it then looks at all extension methods that are within scope to try to find one which matches the required method signature and class. If it finds one, it will pass the instance reference as the first argument to that extension method, then the rest of the arguments, if any, will be passed as subsequent arguments to the extension method. (If the C# compiler doesn’t find any corresponding extension method within scope, it will throw an error.)"* [Patric Ryder]

This is imperative this is understood, a good example is with a included extension method StringExtensions.Contains which when this extension method is invoked and not found a using statement is required. When for instance the using statement is not included but there is a reference in the project and R# (ReSharper is installed) the extension method Contains will be displayed with IntelliSense while without R# Visual Studio will not know about the extension method which in turn will offer to create it for you which is not what is intended.

Test projects

For each language extension method written there should be at least one unit test or a project for UI extension methods so that results expected are verifiable.

Example, test a language extension method which overrides the Contains string method to provide case insensitive option.

[TestMethod]
public void  ContainsTest()
{
    string value = "This is a Sample string";
    // extension method in StringExtensions
    Assert.IsTrue(value.Contains("sample", true));
    Assert.IsTrue(value.Contains("Sample", true));
 
    // extension method in the Framework
    Assert.IsFalse(value.Contains("sample"));
}

The extension  method.

/// <summary>
/// Overload of the standard String.Contains method which provides case sensitivity.
/// </summary>
/// <param name="sender">String to search</param>
/// <param name="pSubString">Sub string to match</param>
/// <param name="pCaseInSensitive">Use case or ignore case</param>
/// <returns>True if string is in sent string</returns>
public static  bool Contains(this string  sender, string  pSubString, bool  pCaseInSensitive)
{
    if (pCaseInSensitive)
    {
        return sender?.IndexOf(pSubString, StringComparison.OrdinalIgnoreCase) >= 0;
    }
    else
    {
        return (bool) sender?.Contains(pSubString);
    }
}

Documentation

Each language extension should be documented. This can be done in a document e.g. Microsoft Word or a better way to document these extension methods is using a documentation tool. 

An easy to use documentation tool is Sandcastle Help File Builder. Sandcastle can be used as a standalone tool or integrated directly into Visual Studio. After a new language extension is written and tested use Sandcastle to create a help file. The learning curve is short and is unforgiving in that it will report when elements of documentation is missing beginning at class level down to method descriptions and parameter information. 

Screenshot from supplied extension methods

Language extension verses custom controls

Given a string needs to be converted to a numeric in a Windows Form project in a TextBox. Option 1, write code in the form in an event to allow only numeric values, with this option the code need be copied and pasted for each TextBox or a shared key event handler. Option 2, create a custom TextBox control which can reside in a class project used in any project. Option 3, create a component derived from implementing IExtenderProvider interface that when placed on a form will provide functionality to limit a TextBox input to numeric. There is no really good use for a language extension method in this case.

A developer may write a string to Int language extension method that can be of assistance only when there is no chance of the string not being able to be converted but those cases are few and far between. There are several language extension methods included in the source code for those who may want one.

Given a DataGridView control with the DataSource property set to a DataTable to gain access to the DataTable cast DataSource to a DataTable in project code, create a custom DataGridView which exposes a DataTable project or better, write a language extension method that works on a standard DataGridView e.g. DataGridView.DataTable();

Casting example.

var dt = (DataTable) dataGridView1.DataSource;

Custom DataGridView.

using System.Data;
 
namespace FormControlsLibrary
{
    public class  DataGridView : System.Windows.Forms.DataGridView
    {
        public DataTable DataTable => (DataTable) DataSource;
    }
}

Language extension method

public static  DataTable DataTable(this DataGridView sender) => (DataTable)sender.DataSource;

Language extension commonality

When possible using the same method on different types keeps code easy to understand and versatile. Using the example above in regards to using a extension method for DataGridView DataSource to DataTable. A developer may create a BindingSource component, set the DataSource to a DataTable followed by assigning the BindingSource to the DataSource of the DataGridView. Using a language extension method a developer would create a DataTable language extension method for a BindingSource.

public static  DataTable DataTable(this BindingSource sender) => (DataTable)sender.DataSource;

Going this route means if the developer can use DataTable extension method against the DataGridView or BindingSource component.

Included source code information

Given a room full of developers all developers will not agree on how the source code included has been written. There is zero obligation to use any of these language extension methods. For those who like the idea of a language extension method include yet are compelled to rewrite one or more methods a developer is more than welcome too but keep in mind when rewriting for each one modified run a unit test against the modified extension method to ensure results are what are expected.

For each project type in the supplied source code they are presented in a solution view. Their physical location are not under the view but instead this is a virtual folder to categorize the projects.

References

Microsoft Extension Methods (C# Programming Guide)  
How to: Implement and Call a Custom Extension Method (C# Programming Guide)
C# 8.0 Features: A Glimpse of the Future - Extension Everything

Summary

This article has provided insight into what makes a good language extension method, how to create and test language extension methods. Before writing a language extension always consider “why”, does it make sense, what will I gain for it has been covered.

Source code

https://github.com/karenpayneoregon/LanguageExtensions

See also

C# unit testing extension methods and validation 
C#: Extension Methods in .NET 
.NET: Defensive data programming (part 1)
Create Mixins with Interfaces and Extension Methods
.NET: Best Approach Implementing equality comparison
Windows DataGridView with inline edit and remove buttons 


Requires

Microsoft Visual Studio (written in 2017)
Microsoft SQL-Server
Sandcastle (optional, if not installed unload the document project from the solution)