Creative use of extension methods in C #

Hello, Habr!



Continuing our exploration of the C # topic, we have translated the following short article for you regarding the original use of extension methods. We recommend that you pay special attention to the last section concerning interfaces, as well as the author's profile .







I'm sure anyone with even a little bit of C # experience is aware of the existence of extension methods. This is a nice feature that allows developers to extend existing types with new methods.



This is extremely handy in cases where you want to add functionality to types that you do not control. In fact, anyone sooner or later had to write an extension for the BCL just to make things more accessible.



But, along with relatively obvious use cases, there are also very interesting patterns tied directly to the use of extension methods and demonstrate how they can be used in a non-traditional way.



Adding Methods to Enumerations



An enumeration is simply a collection of constant numeric values, each assigned a unique name. Although enumerations in C # inherit from the abstract class Enum, they are not interpreted as real classes. In particular, this limitation prevents them from having methods.



In some cases, it can be helpful to program the logic into an enum. For example, if an enumeration value can exist in several different views and you would like to easily convert one to another.



For example, imagine the following type in a typical application that allows you to save files in various formats:



public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}


This enumeration defines a list of formats supported by the application and can be used in different parts of the application to initiate branching logic based on a specific value.



Since each file format can be represented as a file extension, it would be nice if each FileFormathad a method to get this information. It is with the extension method that this can be done, something like this:



public static class FileFormatExtensions
{
    public static string GetFileExtension(this FileFormat self)
    {
        if (self == FileFormat.PlainText)
            return "txt";

        if (self == FileFormat.OfficeWord)
            return "docx";

        if (self == FileFormat.Markdown)
            return "md";

        //  ,      ,
        //      
        throw new ArgumentOutOfRangeException(nameof(self));
    }
}


Which, in turn, allows us to do this:



var format = FileFormat.Markdown;
var fileExt = format.GetFileExtension(); // "md"
var fileName = $"output.{fileExt}"; // "output.md"


Refactoring Model Classes



There are times when you don't want to add a method directly to a class, for example, if you're working with an anemic model .



Anemic models are usually represented by a set of public immutable properties, get-only. Therefore, when adding methods to a model class, you may get the impression that the purity of the code is violated, or you may suspect that the methods refer to some kind of private state. Extension methods do not cause this problem, since they do not have access to the private members of the model and are not by nature part of the model.



So, consider the following example with two models, one representing a closed title list and the other representing a separate title row:



public class ClosedCaption
{
    //  
    public string Text { get; }

    //       
    public TimeSpan Offset { get; }

    //       
    public TimeSpan Duration { get; }

    public ClosedCaption(string text, TimeSpan offset, TimeSpan duration)
    {
        Text = text;
        Offset = offset;
        Duration = duration;
    }
}

public class ClosedCaptionTrack
{
    // ,    
    public string Language { get; }

    //   
    public IReadOnlyList<ClosedCaption> Captions { get; }

    public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions)
    {
        Language = language;
        Captions = captions;
    }
}


In the current state, if we need to get the subtitle string displayed at a particular time, we will run LINQ like this:



var time = TimeSpan.FromSeconds(67); // 1:07

var caption = track.Captions
    .FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);


This really begs some kind of helper method that could be implemented as either a member method or an extension method. I prefer the second option.



public static class ClosedCaptionTrackExtensions
{
    public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) =>
        self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
}


In this case, the extension method allows you to achieve the same as the usual one, but gives a number of non-obvious bonuses:



  1. It is clear that this method works only with public members of the class and does not change its private state in some mysterious way.
  2. Obviously, this method simply allows you to cut the corner and is provided here for convenience only.
  3. This method belongs to a completely separate class (or even assembly) whose purpose is to separate data from logic.


In general, when using the extension method approach, it is convenient to draw a line between necessary and useful.



Making Interfaces Versatile



When designing an interface, you always want the contract to be kept as small as possible because it makes it easier to implement. It helps a lot when the interface provides functionality in the most generalized way, so that your colleagues (or yourself) can build on it to handle more specific cases.



If this sounds nonsense to you, consider a typical interface that saves a model to a file:



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);
}


Everything works fine, but in a couple of weeks a new requirement may arrive: the classes that implement IExportServicemust not only export to a file, but also be able to write to a file.



Thus, to fulfill this requirement, we add a new method to the contract:



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);

    byte[] SaveToMemory(Model model);
}


This change just broke all of the existing implementations IExportServiceas they now all need to be updated to support writing to memory as well.



But, in order not to do all this, we could have designed the interface a little differently from the very beginning:



public interface IExportService
{
    void Save(Model model, Stream output);
}


In this form, the interface forces you to write the destination in the most generalized form, that is, this Stream. Now we are no longer limited to files when working and can also target various other output options.



The only drawback of this approach is that the most basic operations are not as simple as we are used to: now we have to set a specific instance Stream, wrap it in a using statement and pass it as a parameter.



Fortunately, this drawback is completely nullified when using extension methods:



public static class ExportServiceExtensions
{
    public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)
    {
        using (var output = File.Create(filePath))
        {
            self.Save(model, output);
            return new FileInfo(filePath);
        }
    }

    public static byte[] SaveToMemory(this IExportService self, Model model)
    {
        using (var output = new MemoryStream())
        {
            self.Save(model, output);
            return output.ToArray();
        }
    }
}


By refactoring the original interface, we made it much more versatile and didn't sacrifice usability by using extension methods.



Thus, I find extension methods an invaluable tool in keeping the simple simple and turning the complex into the possible .



All Articles