C# Language extensions

Karen Payne - May 8 '23 - - Dev Community

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.

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.)"

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.

Simple string example

The task is to remove all whitespace from a string, this code to accomplish this can be done inline with the task but takes away from the issue being taken care of so let's use a language extension.

  • Write the code to, in this case remove all whitespace in perhaps a console project, make sure it works.
  • Create a class with the appropriate modifier e.g. internal, public etc.
  • Make the class static and give it a useful name e.g. StringExtensions.
public static class StringExtensions
{

}
Enter fullscreen mode Exit fullscreen mode

Now create the method

🛑 this method is not optimal, see end of article for a better version.

  • Use public modifier for the method
  • Argument 1, this which reference to the type
  • Argument 2, the type for the method
public static class StringExtensions
{
    /// <summary>
    /// Remove all white space in a string, at start, end and in-between
    /// </summary>
    /// <param name="sender"></param>
    /// <returns>a string with no whitespace</returns>
    public static string RemoveAllWhiteSpace(this string sender)
        => sender
            .ToCharArray().Where(character => !char.IsWhiteSpace(character))
            .Select(character => character.ToString())
            .Aggregate((value1, value2) => value1 + value2);
}
Enter fullscreen mode Exit fullscreen mode

In the project to use the extension, try it out.

static void RemoveWhitespace()
{
    string value = " This   is  filled with white space 1  2  3";
    Console.WriteLine($"[{value.RemoveAllWhiteSpace()}]");
}
Enter fullscreen mode Exit fullscreen mode

Result should be [Thisisfilledwithwhitespace123] where in this case brackets are use to show clearly no white space at the start and end of the string.

Clear code, less code

We want to deconstruct a DateOnly variable, conventual code.

DateOnly dateOnly = new DateOnly(2023,1,15);
Console.WriteLine($"{dateOnly.Month} {dateOnly.Day} {dateOnly.Year}");
Enter fullscreen mode Exit fullscreen mode

With a language extension

public static class DateOnlyExtensions
{
    public static void Deconstruct(this DateOnly date, out int day, out int month, out int year) =>
        (day, month, year) = (date.Day, date.Month, date.Year);
}
Enter fullscreen mode Exit fullscreen mode

Usage where after typing

DateOnly dateOnly = new DateOnly(2023,1,15);

Visual Studio will offer to deconstruct the variable for you.

Visual Studio suggestion

Once deconstructed

var (day, month, year) = new DateOnly(2023,1,15);
Enter fullscreen mode Exit fullscreen mode

Which means we can write

Console.WriteLine($"{month} {day} {year}");
Enter fullscreen mode Exit fullscreen mode

Generic extensions

Extension Methods (C# Programming Guide)

Generics introduces the concept of type parameters to .NET, which make it possible to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code.

Example 1

There are time when the task needs to be done for int, double, decimal and DateTime. Conventional wisdom has a developer write code for each time.

You are asked to write code to see value falls into a range.

The following handles all these cases.

public static class GenericExtensions
{
    public static bool Between<T>(this T value, T lowerValue, T upperValue) where T : struct, IComparable<T>
        => Comparer<T>.Default.Compare(value, lowerValue) >= 0 &&
           Comparer<T>.Default.Compare(value, upperValue) <= 0;
}
Enter fullscreen mode Exit fullscreen mode

DateOnly example (can also use DateTime)

var startDate = new DateOnly(2023, 1, 15);
var endDate = new DateOnly(2023, 1, 20);
var middleDate = new DateOnly(2023, 1, 17);

Console.WriteLine(
    middleDate
        .Between(startDate, endDate));
Enter fullscreen mode Exit fullscreen mode

Using an int

int lower = 2;
int upper = 8;
int middle = 5;

Console.WriteLine(middle.Between(lower,upper));
Enter fullscreen mode Exit fullscreen mode

Example 2

Most developers will need to work with json data, if this is not something done offend than in most cases a developer will need to read the documentation.

You need to write a list to a json file formatted.

The extension

  • Argument one T a type, in this case a list
  • Argument two filename a file to write data too.
  • Argument three format pass false to not format, the default is true which means to format the json data in argument one.
public static class GenericExtensions
{

    /// <summary>
    /// Serialize string to specific type
    /// </summary>
    /// <typeparam name="T">Type to serialize</typeparam>
    /// <param name="sender"></param>
    /// <param name="fileName">File name to write too</param>
    /// <param name="format">Use formatting for serialization or not</param>
    /// <returns></returns>
    public static (bool result, Exception exception) SerializeToFile<T>(this List<T> sender, string fileName, bool format = true)
    {

        try
        {

            var options = new JsonSerializerOptions { WriteIndented = true };
            File.WriteAllText(
                fileName, 
                JsonSerializer
                    .Serialize(sender, (format ? options : null)));

            return (true, null);

        }
        catch (Exception e)
        {
            return (false, e);
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

Usage

Our model

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Gender { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Mocked data

public class Mocked
{
    public static List<Person> People() =>
        new()
        {
            new()
            {
                Id = 1, 
                FirstName = "Anne", 
                LastName = "White", 
                Gender = "F"
            },
            new()
            {
                Id = 2, 
                FirstName = "Mike", 
                LastName = "Smith", 
                Gender = "M"
            }
        };
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together

List<Person> people = Mocked.People();
people.SerializeToFile("people.json", true);
Enter fullscreen mode Exit fullscreen mode

Output

[
  {
    "Id": 1,
    "FirstName": "Anne",
    "LastName": "White",
    "Gender": "F"
  },
  {
    "Id": 2,
    "FirstName": "Mike",
    "LastName": "Smith",
    "Gender": "M"
  }
]
Enter fullscreen mode Exit fullscreen mode

If false were passed for formatting.

[{"Id":1,"FirstName":"Anne","LastName":"White","Gender":"F"},{"Id":2,"FirstName":"Mike","LastName":"Smith","Gender":"M"}]
Enter fullscreen mode Exit fullscreen mode

Example 3

This is for Windows Forms, throughout the years I found this example helpful in testing generics simple because of the visualization of a CheckListBox displaying a list of T where the extension method provides a way to get checked items no matter of the underlying data source so long as it's a strong typed list.

A demo is include in source code.

public static class CheckedListBoxExtensions
{
    /// <summary>
    /// Get checked items as <see cref="T"/>
    /// </summary>
    /// <typeparam name="T">Model</typeparam>
    /// <param name="sender">CheckedListBox</param>
    /// <returns>List if one or more items are checked</returns>
    public static List<T> CheckedList<T>(this CheckedListBox sender)
        => sender.Items.Cast<T>()
            .Where((item, index) => sender.GetItemChecked(index))
            .Select(item => item)
            .ToList();
Enter fullscreen mode Exit fullscreen mode

Example 4

Working with temp data in an ASP.NET Core or Razor Pages project.

public static class TempDataHelper
{
    /// <summary>
    /// Put an item into TempData
    /// </summary>
    /// <typeparam name="T">Item type</typeparam>
    /// <param name="sender">TempData</param>
    /// <param name="key">Used to retrieve value with <see cref="Get{T}"/> </param>
    /// <param name="value">Value to store</param>
    public static void Put<T>(this ITempDataDictionary sender, string key, T value) where T : class
    {
        sender[key] = JsonConvert.SerializeObject(value);
    }
    /// <summary>
    /// Get value by key in TempData
    /// </summary>
    /// <typeparam name="T">Item type</typeparam>
    /// <param name="sender">TempData</param>
    /// <param name="key">Used to retrieve value</param>
    /// <returns>Item</returns>
    public static T Get<T>(this ITempDataDictionary sender, string key) where T : class
    {
        sender.TryGetValue(key, out var unknown);
        return unknown == null ? null : JsonConvert.DeserializeObject<T>((string)unknown);
    }
}
Enter fullscreen mode Exit fullscreen mode

Put example

public async Task<IActionResult> OnPostToIndex()
{
    Random rnd = new();

    await LoadPeople();

    var person = Person.MinBy(r => Guid.NewGuid());

    TempData["SomeValue"] = rnd.Next(52);
    TempData["UserName"] = "billyBob";
    TempData.Put("person", person);

    return RedirectToPage("/Index");
}
Enter fullscreen mode Exit fullscreen mode

Using Get

public void OnGet()
{

    // test values set in ListPeople page for exact count
    if (TempData.Count == 3)
    {
        _logger.LogInformation("OnGet IndexPage");
        SomeValue = int.Parse(TempData[nameof(SomeValue)].ToString()!);
        UserName = TempData[nameof(UserName)].ToString();
        Person = TempData.Get<Person>("person");
    }

    // here we only need to above once so clear the items
    TempData.Clear();
    // since we just cleared TempData the count will be zero.
    TempDataCount = TempData.Count;

}
Enter fullscreen mode Exit fullscreen mode

Working with data

The task is to provide a dynamic method to order a list of customers.

  • Order by First name
  • Order by Last name
  • Order by Country name in a navigation
  • Allow Ascending or Descending

The model

public partial class Customers
{

    public int CustomerIdentifier { get; set; }
    public string CompanyName { get; set; }
    public int? ContactId { get; set; }
    public int? CountryIdentifier { get; set; }
    public int? ContactTypeIdentifier { get; set; }

    public virtual Contacts Contact { get; set; }
    public virtual ContactType ContactTypeNavigation { get; set; }
    public virtual Countries CountryNavigation { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The extension method which can be improved on which will be in an upcoming article.

public static class OrderingHelpers
{
    /// <summary>
    /// Provides sorting by string using a key specified in <see cref="key"/> and if the key is not found the default is <see cref="Customers.CompanyName"/>
    /// </summary>
    /// <param name="query"><see cref="Customers"/> query</param>
    /// <param name="key">key to sort by</param>
    /// <param name="direction">direction to sort by</param>
    /// <returns>query with order by</returns>
    /// <remarks>Fragile in that if a property name changes this will break</remarks>
    public static IQueryable<Customers> OrderByString(this IQueryable<Customers> query, string key, Direction direction = Direction.Ascending)
    {
        Expression<Func<Customers, object>> exp = key switch
        {
            "LastName" => customer => customer.Contact.LastName,
            "FirstName" => customer => customer.Contact.FirstName,
            "CountryName" => customer => customer.CountryNavigation.Name,
            "Title" => customer => customer.ContactTypeNavigation.ContactTitle,
            _ => customer => customer.CompanyName
        };

        return direction == Direction.Ascending ? query.OrderBy(exp) : query.OrderByDescending(exp);

    }
}
Enter fullscreen mode Exit fullscreen mode

Usage

public static async Task<List<Customers>> SortByPropertySecondAttempt(string propertyName)
{
    await using var context = new NorthWindContext();

    return await context.Customers
        .Include(c => c.CountryNavigation)
        .OrderByString(propertyName, Direction.Ascending)
        .ToListAsync();
}
Enter fullscreen mode Exit fullscreen mode

So how do we get the property names?

Part of a future article but we are using an extension method

/// <summary>
/// Get properties for a model
/// </summary>
/// <param name="context">active DbContext</param>
/// <param name="modelName"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static List<string> GetModelPropertyNames(this DbContext context, string modelName)
{

    if (context == null) throw new ArgumentNullException(nameof(context));

    var entityType = GetEntityType(context, modelName);

    var list = new List<string>();

    IEnumerable<IProperty> properties = 
        context.Model.FindEntityType(entityType ?? 
                                     throw new InvalidOperationException())
            !.GetProperties();


    foreach (IProperty itemProperty in properties)
    {
        list.Add(itemProperty.Name);
    }

    return list;

}
Enter fullscreen mode Exit fullscreen mode

Maintenance

There will be times when a developer creates an extension method, test it and leave as is. When time permits go back and see if the code could be optimizes.

Example, the following extension method removes all whitespace from a string.

public static string RemoveAllWhiteSpace(this string sender)
    => sender
        .ToCharArray().Where(character => !char.IsWhiteSpace(character))
        .Select(character => character.ToString())
        .Aggregate((value1, value2) => value1 + value2);
Enter fullscreen mode Exit fullscreen mode

The code can be optimized!

public static string RemoveWhitespace(this string input) =>
    new(input.ToCharArray()
        .Where(c => !char.IsWhiteSpace(c))
        .ToArray());
Enter fullscreen mode Exit fullscreen mode

Another example from above, OrderByString extension method for EF Core to sort a list, the code works but can be improved and will show this in an upcoming article.

Usually what happens, as a developer grows they find better ways to write code so take time to go back and review code.

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.

Exensionmethods.net

Check out Exensionmethods.net which has many useful extension methods but make sure to test them out.

Summary

Extension methods used properly can make life easier and makes it easier to read code.

Requires

Microsoft Visual Studio 2022 or higher

Source code

Clone the following GitHub repository

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .