Extension Methods for .NET Standard Library Types

    Probably, almost every .NET developer has come across cases when, for the convenience of coding routine actions and reducing the boilerplate code when working with standard data types, the capabilities of the standard library are not enough.


    And in almost every project, assemblies and namespaces of the form Common, ProjectName.Common, etc. appear that contain additions for working with standard data types: Enums enumerations, Nullable structures, strings and collections - IEnumerable enumerations, arrays, lists, and collections proper.


    As a rule, these additions are implemented using the extension methods mechanism. Often one can observe the existence of monad implementations, also built on the mechanism of extension methods.


    (Looking ahead - we will consider issues that unexpectedly arise, and which you may not notice when you create your own extensions for IEnumerable, and work is being done with IQueryable)


    The writing of this article was inspired by reading a long-standing article-translation of the Check for empty transfers and the discussion that developed.


    The article is old, but the topic is still relevant, especially since a code similar to the example from the article had to be encountered in real work from project to project.


    In the original article, a question was raised, which inherently concerns the whole Common-libraries added to work projects.


    The problem is that such extensions in product projects are added hastily, because developers are engaged in the creation of new features, and no time is allocated for creating, thinking through and debugging the basic infrastructure.


    In addition, as a rule, the developer, adding the Common complement necessary for him, creates it so that this addition is tailored for cases from its features, and does not think that since this is a general addition, it should be as abstract as possible from objective logic and be universal - as is done in standard platform libraries.


    As a result, in numerous Common-subfolders of projects, the deposits of the code given in the original article are obtained :


    public void Foo(IEnumerable items) 
    {
     if(items == null || items.Count() == 0)
     {
      // Оповестить о пустом перечислении
     }
    }

    The author pointed out a problem with the Count () method and suggested creating such an extension method:


    public static bool IsNullOrEmpty(this IEnumerable items)
    {
      return items == null || !items.Any();
    }

    But the presence of such a method does not solve all the problems:


    • In the comments, a discussion arose on the topic that the Any () method does one iteration, which can lead to a problem when the subsequent iteration over the collection (which is assumed after checking IsNullOrEmpty) is performed not from the first, but from the second element, and the objective logic of does not recognize this.
    • To which an objection was received that the Any () method creates a separate iterator for verification (note that this is a certain overhead).

    Now let’s note that all standard .NET collections, except for the actually “infinite” IEnumerable sequence - arrays, lists, and collections directly - implement the standard IReadOnlyCollection interfaceproviding the Count property - and no overhead iterators are needed.


    Thus, it is advisable to create two extension methods:


    public static bool IsNullOrEmpty(this IReadOnlyCollection items)
    {
      return items == null || items.Count == 0;
    }
    public static bool IsNullOrEmpty(this IEnumerable items)
    {
      return items == null || !items.Any();
    }

    In this case, when calling IsNullOrEmptythe appropriate method will be chosen by the compiler, depending on the type of object for which the extension is being called. The call itself will look the same in both cases.


    However, later in the discussion, one of the commentators indicated that probably for IQueryable (the interface of the "infinite" sequence for working with database queries, inheriting from IEnumerable) the most optimal would be just calling the Count () method.


    This version requires verification, including checks for working with different ORMs - EF, EFCore, Linq2Sql, and if so, there is a need to create a third method.


    In fact, for IQueryable There are extension-implementations of Any (), Count () and other methods of working with collections (class System.Linq.Queryable), which are designed to work with ORM, in contrast to similar implementations for IEnumerable (class System.Linq.Enumerable).


    In this case, probably, the Queryable version of Any () works even more optimally than the Queryable version Count () == 0 .


    To call the necessary Queryable versions of Any () or Count (), if we want to call our IsNullOrEmpty check, we will need a new method with IQueryable-input parameter.


    Thus, you need to create a third method:


    public static bool IsNullOrEmpty(this IQueryable items)
    {
      return items == null || items.Count() == 0;
    }

    or


    public static bool IsNullOrEmpty(this IQueryable items)
    {
      return items == null || !items.Any();
    }

    As a result, in order to implement a simple null-safe check of collections for "emptiness" that is correct for all cases (for all?), We had to conduct a little research and implement three extension methods.


    And if at the initial stage you create only a part of the methods, for example, only the first two (these methods are not needed; you need to make product features), then this could happen:


    • As soon as these methods have appeared, they begin to be used in the product code.
    • At some point, calls to Enumerable versions of IsNullOrEmpty penetrate the ORM code, and these calls will certainly not work optimally.
    • What to do next? Add Queryable versions of methods and rebuild the project? (We add only new extension methods, we don’t touch the product code - after rebuilding, switching to the necessary methods will happen automatically.) This will necessitate regression testing of the entire product.

    For the same reason, it is advisable to implement all these methods in one assembly and one namespace (it is possible in different classes, for example, EnumerableExtensions and QueryableExtensions), so that if we accidentally disconnect the namespace or assembly, we will not return to the situation when IQueryablecollections work using regular Enumerable extensions.


    In my opinion, the abundance of such extensions in almost every project indicates the insufficient development of the standard library and the platform model as a whole.


    Part of the problems would be automatically resolved if there was support for Not Nullability in the platform, another part - if the standard library had more that take into account a wider range of extension cases for working with standard data types.


    Moreover, implemented in a modern way - it is in the form of extensions using generalizations (Generics).


    We will talk more about this in the next article.


    PS What is interesting, if you look at Kotlin and its standard library, during the development of which the experience of other languages ​​was clearly carefully studied, first of all, in my opinion - Java, C # and Ruby, then you can easily find just these things - Not Nullability and an abundance of extensions, in the presence of which there is no need to add your own "bicycle" implementation of micro-libraries to work with standard types.


    Also popular now: