Welcome Builders!

How the Omnisharp roadmap affects .NET in Cooperations

The announcement to make the LSP server host of OmniSharp closed source hits me hard. I cannot understand why the DevDiv leadership of Microsoft is so willingly and ongoingly destroying the reputation of .NET (Core). .NET Core is one of the three core pillars (VS Code, TypeScript and .NET Core) which generated respect for Microsoft in the wider programming community within the last 10 years.

I am a .NET fanboy but I have a perspective on the broader spectrum of programming languages. I am an architect, decision maker/influencer and regularly involved in questions on whether Visual Studio Subscriptions are needed. I am not in a Microsoft shop, I am not in the silicon valley and I am in a conservative polyglot business.

What defines a state of the art programming language

In the past, programming languages consisted of a runtime, compiler and debugger. Microsoft's own Anders Hejlsberg (of Pascal, C#, TypeScript) explained in an awesome video named "Modern Compiler Construction" how previously considered IDE features like code completion, syntax highlighting, code analysis or code refactoring are now part of the compiler. C# (Roslyn) and especially TypeScript have been (modern day) forerunners of this. This combined with the general industry adoption of the language server protocol developed by the VS Code (Monaco?) team basically defines what a modern programming language is:

  • A modern, modular, non-black-box compiler
  • A debugger
  • A rich out-of-the-box library
  • A LSP server written in the same programming language

.NET is awesome in all of them: The compiler is awesome, the debugger is awesome, the base class library is awesome and the LSP server ... well ... is there. But make no mistake: The other languages have this whole portfolio as well or are on the way there (Java, JavaScript/TS, Python, Go, Rust, PHP, ...).

The core attribute of most application programming languages is developer productivity. But that is a full spectrum reaching from simple auto-completion to AI-guided auto-coding. For me the hierarchy is the following

Basic Productivity

  1. Syntax Highlighting
  2. Meaningful errors during compilation / editing
  3. Debugging (stepping, breakpoints, ...)
  4. Correct context specific auto-completion (as in IntelliSense)
  5. Code Navigation
  6. Refactoring / Code Fixes

Advanced Productivity

  1. Advanced Debugging (as in IntelliTrace, break and edit, hot reload, ...)
  2. AI / statistical guidance (as in GitHub CoPilot, IntelliCode, ...)

Super Advanced Productivity

  1. Visual Designers (remember why we bought Visual Studio in the 90s)

Everything in basic productivity is in 2022 elementary state of the art and comes for free (rhetorically up to here: free as in beer).

What makes a programming language adopted in a coorperation?

When making decisions about programming languages, we decision makers have to consider ...

hard factors

  • The Architect asks: Does the programming language fullfill the needs?
  • The R&D lead asks: Can I hire easily for this programming language? Does my team know this language?

soft factors

  • The Apple, Linux, Web, or Windows fanboy / IT admin asks: Does it run on my platform?
  • The R&D lead asks: Do I need licenses for it?
  • The careful person asks: Is it free software? Is it Open Source Software? Can I own it? Can I rely on it?

So in a free choice situation it boils down to these soft decision factors.

On platform coverage: The gigantic success of VS Code and .NET Core have opened up the platform coverage topic for C#/F#. .NET can freely compete with Java due to this.

The actual trigger for the announcement is further investment into that. That is good and everyone likes it.

On licenses: For C#/F# we could always argue that there is VS Code and Omnisharp and "everything" is MIT licensed. We decision maker buy anyway the Visual Studio licenses because of increased productivity. The same applies with Java on the Eclipse / JetBrains IntelliJ split.

And the trigger for the announcement is further investment into that. That is good and everyone likes it.

However, there is the fundamental split between basic productivity and advanced productivity and our willingness to pay for it.

✅ Are we willing to pay for advanced productivity? Answer: Yes! Evidence: Many many Visual Studio and JetBrains subscription.

❌ Are we willing to pay for basic productivity? Answer: No! Evidence: Adoption of Omnisharp and Creation of Visual Studio Community.

Why we do not pay for basic productivity features: because it is 2022 and there are excellent alternatives out there. Java has all the whistles and most others have at least the basic productivity features.

On open vs. closed source: How deep can I inspect it when something misbehaves? Can I fix it? Am I dependent and locked on a single vendor (for price hickups, discontinuations, ...)? Can I archive the package for 30 years and fix it myself? Is it secure?

❌ All of these questions flip with this announcement. There is no trust in big cooperations to either discontinue a product, not care about a product or extort money from us developers. We developer got burned by Apple, Google and Microsoft on every opportunity when it comes to this. The only trust establishing situation is real open source. For basic productivity the full end to end stack needs to be open source (incl. debugger and "LSP Tools Host") as in MIT/Apache/GPL licensed or viable alternatives needs to be present (as in Eclipse for Java). Advanced productivity can be always be optional from this safeguarding perspective.

A way out

This is the simple part

  1. Make the "LSP Tools Host" and the .NET debugger open source (license wise).
  2. Make both of them pluggable with Microsoft commercial plugins for the advanced productivity. I know, especially for the debugger, that this is an hard thing.
  3. Sell us a "C# (Commercial)" with advanced productivity edition next a fully free "C# (Community)" with basic productivity edition (free as in beer AND in open source license).

I do not see how Microsoft can loose intellectual property, thought leadership or market share to anyone else. Not on the short term and not on the long term when trust is established.

Considering, what happenend with Pylance (and IMHO PHP language server): The above should be a playbook for any language not only .NET/C#/F#.

Appendix A: An architecture side note

  • There is an awesome LSP protocol client/server library as part of the OmniSharp library. No matter what, do not close source this even if it is extended by proprietary Microsoft extensions. It is a human readable protocol. The .NET community builds language servers beyond just the languages C#/F#.
  • Do not break the Roslyn project and the public availability of code analyzers/fixes (and other aspects of the "Modern Compiler Construction"). It is a modern compiler and not only an AST exporter.
  • I assume the "LSP Tool Host" is nothing else than interacting with and editor based on text via LSP/DSP and then forwarding the events into the commercial and non-commercial plugins. Why is a stable, reliable, easy, multi-provider language/debugging server such a secret. Especially when the DSP and LSP are such a thin wrapper around human-eye visible, standardized text editor features.

Appendix D: Disclaimers

  • I am not speaking for my employer
  • I am not advocating in favor of free software vs. open source software. MIT is for me, in this local context free software.

Appendix E: An ethical side note

  • There are countless contributors who have contributed to OmniSharp, Hot Reload and the .NET Foundation (as an entity). Do not screw them. Do not steal their work (intelectionally, morally, reputation or physically).
  • We developers very well accept Microsoft's need to earn money. Go Open Core with .NET. Be open about it, be smart about it (see all of above) and include the community on the road. We developer have zero tolerance for this kind of messaging (remember: we deal with a CPU who is not ambivalent in its outcome: it is either true or false) where we are told half of the relevant information. We are now in the third communication debacle within a year (.NET Foundation, Hot Reload, "LSP Tools Host") just because Microsoft management is not willing to tell us their roadmap and real intention.

Extension Methods

Extension Methods in C# are a crucial element of modern .NET. They are the foundation of LINQ and used everywhere. However, the are sometimes considered as a bad smell when it comes to code reviewing. In this article I want to discuss the concept of extension methods and its position in the OO patterns & principles.

Note: Extension Methods are often used to build domain specific languages (might it be a SwiftUI-alike, a mocking specification system, ...). Everything goes in this space. The target there is the newly constructed language. In this article, I discuss the integration into the regular C# programming language and not a specialization of it.

SOLID Principles

SOLID principles are a corner stone of object oriented design.

  • S ingle Responsibility Principle (SRP): "There should never be more than one reason for a class to change." In other words, every class should have only one responsibility.
  • O pen Closed Principle (OCP): "Software entities ... should be open for extension, but closed for modification."
  • L iskov Substitution Principle (LSP): "Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it". See also design by contract.
  • I nterface Segregation Principle (ISP): "Many client-specific interfaces are better than one general-purpose interface."
  • D ependency Inversion Principle (DIP): "Depend upon abstractions, not concretions."

-- via Wikipedia

Soft Skills (of Code)

Under the world of patterns and principles there are also soft skills 😀.

  • Readability: Code is read 100 times as often as it is created or modified. It is read in full fledged editors or in a diff tool on the command line without even syntax highlighting. Regular (as in 99% of) code has to be optimized for readability.
  • Usability: When writing function with the intention of other developers using it, usability is a concern. The definition of the function as well as its integration into the workflow of the user in the IDE matters here.

The Evaluation

So are exension methods violating these principles? Are they a bad thing in software design?

Let us dissect different use cases!

  • Extending fundamental base types: Adding an extension method to a base type.

    string foo = "Hello World";
    
    var hash = foo.HashSha1();
    
    // vs. static method
    var has = Sha1.Hash(foo);
    

    ❌ Single Responsibility: There is no space for hashing in strings. .WordCount() or .IsASCII() maybe, because the contribute to the responsibility to represent text. But Hash is a different domain. The responsibility of hashing and the responsibility of text representation does not intersect. This usage only moves the argument from the braces to the context.

    ❌ Readability: There is no gain versus the usage in traditional static methods. The extension method feels like a transformation, the static method invocation like a function call. Both are fundamentally well understood concept when reading code. Both are valid patterns in object oriented programming and functional programming. The significant negative thing the extension method does is hiding the ownership of the function.

    ❌ Usability: There is no benefit in the general usability either. Auto-Completion would be cluttered with suggestions of countless domains. Namespace (and by extend static class names) with using, static using and global using have a rationale to exist, which is de-cluttering the global namespace and resolving potential conflicts in naming. Extension methods are typically attached to more generic namespaces to unfold their availability.

  • Throw helpers: Simplify your parameter guards by using one-line-extension methods.

    public void Execute(string command, int count)
    {
        command.ThrowIfNullOrEmpty();
        count.ThrowIfSmallerThan(0);
        // ...
    

    ❌ Single Responsibility: See above.

    ❌ Readability: See above.

    ❌ Usability: See above. It is even worse here, since the contributed functions are only for a special purpose which is only used at the beginning of a function.

  • Extending the capabilities: Adding new capabilities to a type.

    public interface ILogger { void Log(string text); }
    
    ILogger log = GetLogger();
    log.LogDebug();
    

    ✅ Single Responsibility Principle: Not violated. LogDebug contributes to the responsibility of ILogger.

    ✅ Open-Close Principle: ILogger is not modified (it remains closed). However, C# extension methods make it Open for extensions.

    ✅ Liskov Substitution Principle: Another implementation of ILogger (or another derived class of a default logger) can be provided without being influenced by the extension mehtod. The extension methods, like other consumers expect an unchanged behavior. Another consumer of ILogger interface (old and new implementation) would not be influenced.

    ✅ Interface Segregation Principle: LogDebug and Log are contracted separately from each other (they come from different types), so the interface was segregated and can be modified independently.

    🕵️‍♀️ Dependency Inversion Principle: There is no abstraction here. The extension method cannot be provided by a third party (like a DI container) and abstracted by an interface. It is a static method.

    ✅ Readability: The contributed extended method intents to extend the capability of the original type. It belongs to the type in regards of the SRP. Additionally - in this example and as a recommendation - the extension method naming is adjusted to the target type helping the reader to understand the purpose.

    ✅ Usability: The extension method improves the usability by making the functionality available with Intellisense.

  • Bridging Domains: Adding capabilities to one domain by attaching another domain.

    void Configure(IServiceCollection services)
    {
        services
            .AddLogging()
            .AddMvc();
    }
    

    ✅ Single Responsibility Principle: .AddLogging() contributes to the DI Container building capability of IServiceCollection. It also has the responsbility of initializing the ILogger infrastructure withing the Logging subsystem. Therefore, .AddLogging() has no place in implementation or the interface of IServiceCollection but is an ideal candiate for extension methods (even in the same assembly and direct next to the extended type).

    Assembly: Logging Assembly: DependencyInjection IServiceCollection + AddSingleton + AddTransient + BuildServiceProvider + AddLogging

    ✅ Open-Close Principle: IServiceCollection is not modified as Logging does not contribute to its domain and there is no need to change it since consumers can only use .AddSingleton() or .AddTransient(). The interface remains closed. However, C# extension methods make it Open for extensions.

    ✅ Liskov Substitution Principle: See above.

    ✅ Interface Segregation Principle: See above. Extension methods are an valid method to segregate the interface into smaller parts (here the segregated specialization into the bridged technology). Extensions methods also do not contribute to the single-inheritance limitation (which extension derives in which order and are all known) and effectively form segregated interfaces.

    Assembly: Logging Assembly: DependencyInjection Assembly: AspNetCore IServiceCollection + AddSingleton + AddTransient + BuildServiceProvider + AddLogging + AddMvc

    🕵️‍♀️ Dependency Inversion Principle: See above.

    ✅ Readability: See above.

    ✅ Usability: See above.

Summary

Not all extension method use cases are really good usages when it comes to object-oriented programming. However, there are plenty of good ones.

Note: I will continually update this article when I see proper use cases which are interesting to document.


On Building a Middleware Framework

I am fascinated by Middleware Stacks. Middlewares help us to customize a function invocation (typically a request handler) with additional behavior before and after the invocation of function. Popular examples are authentication, logging or exception handling middlewares. Often they handle non-functional requirements (like logging, security, ...).

As a .NET developer I use daily the prominent middleware stack in ASP.NET Core. While beautifully abstracted and designed, there is however one big caveat: It is bound to HTTP request/response. Due to that limitation I built violetgrass/middleware.

In this article I try to discuss the various elements of a ASP.NET Core inspired middleware stack implemented in pure .NET.

The delegate

A middleware is a stackable function. The function definition is called the MiddlewareDelegate (ASP.NET Core: RequestDelegate).

public delegate Task MiddlewareDelegate<TContext>(TContext context) where TContext : Context;

The function expose the following characteristics:

  • Is is asynchronous, since most likely, some part of the stack needs to access slowlier resources (e.g. a file system).
  • It receives an invocation context (essentially the input and output of the function). (ASP.NET Core: HttpContext).
  • It is a call without a return value.
  • It is stateless on its own (it is a function not an interface for a class).

A simple message handler can be represented in the MiddlewareDelegate.

async Task HandleAsync(Context context)
{
    var message = GetMessageFromContext(context);
    Console.WriteLine(message)
}

MiddlewareDelegate<Context> messageHandler = HandleAsync;

var message = GetMessage();
await messageHandler(message); // process the message.

A middleware however, is not only the final handler, but essentially everything in the middle between the dispatching invoker and the terminal handler. In simple cases, this could be added programmatically.

async Task HandleAsync(Context context) { /* see above */ }
async Task LogAsync(Context context)
{
    Log.Write("Before");
    await HandleAsync(context);
    Log.Write("After");
}
async Task CatchExceptionAsync(Context context)
{
    try
    {
        await LogAsync(context);
    }
    catch { /* ... */ }
}

MiddlewareDelegate<Context> messageHandler = CatchExceptionAsync;

var message = GetMessage();
await messageHandler(message); // try-catch, log and process the message.

This method of coding exposes some issues

  • The composition of the functions influences the actual code of the functions.
  • There is not methodology to integrate third parties or have out-of-the-box functionality.

The building of the middleware (stack)

The purpose of using a middleware framework is to enable second and third party integrations into an efficient invocation stack. This includes all attributes of regular method stacking (like controlling the code before and after the invocation and handlings exceptions).

In a first step, the invocation of the next wrapped function needs to be parameterized.

async Task LogAsync(MiddlewareDelegate<Context> next, Context context)
{
    Log.Write("Before");
    await next(context);
    Log.Write("After");
}

However, that would imply that the delegate is recursive and would differ between middleware (which need next) and the terminal handler (which does not need next). Closures to rescue which can bind additional variables not represented as function parameters. Closures need to be created by another function which holds the "closured" scope ...

MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
{
    return async (Context context) => // = MiddlewareDelegate<Context>
    { 
        Log.Write("Before");
        await next(context);
        Log.Write("After");
    };
}

// or in modern C# with local functions instead of lambdas.
MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
{
    return LogAsync;

    async Task LogAsync(Context context)
    {
        Log.Write("Before");
        await next(context);
        Log.Write("After");
    }
}

// or if you like expression bodied member
MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
    => async context => {  Log.Write("Before"); await next(context); Log.Write("After"); };

// or currying and first class functions
Func<MiddlewareDelegate<Context>, MiddlewareDelegate<Context>> LogFactory = next => async context => {  Log.Write("Before"); await next(context); Log.Write("After"); };

This function which builds the middleware is called a middleware factory (Func<MiddlewareDelegate<TContext>, MiddlewareDelegate<TContext>>). It is a function which receives the next element in the middleware stack to built and emits a function which represent the current middleware and all middleware later in the stack.

You can now write ..

var messageHandlerStep3 = HandleAsync;
var messageHandlerStep2 = LogFactory(messageHandlerStep3);
var messageHandlerStep1 = CatchExceptionFactory(messageHandlerStep2);

var message = GetMessage();
await messageHandlerStep1(message); // try-catch, log and process the message.

So now let us build a fancy builder infrastructure for it.

A IMiddlewareBuilder<TContext> (ASP.NET Core: IApplicationBuilder) collects a set of middleware factories.

public IMiddlewareBuilder<TContext> Use(Func<MiddlewareDelegate<TContext>, MiddlewareDelegate<TContext>> middlewareFactory)
{
    _factories.Add(middlewareFactory);

    return this;
}
public MiddlewareDelegate<TContext> Build()
{
    _factories.Reverse();

    MiddlewareDelegate<TContext> current = context => Task.CompletedTask; // safeguard

    foreach (var middlewareFactory in _factories)
    {
        current = middlewareFactory(current);
    }

    return current;
}

Note: Ignoring all the interface definitions and additional features beyond middleware, this above is the complete business logic of violetgrass/middleware.

The above Build method iterate each factory (in reverse order) and throw the built middleware function of the current factory as an input to the factory method of the next layer. An Build invocation might look like that ...

var messageHandler = new MiddlewareBuilder<Context>()
    .Use(CatchExceptionFactory)
    .Use(LogFactory)
    .Use(next => HandleAsync) // no use of next
    .Build();
    
var message = GetMessage();
await messageHandler(message); // try-catch, log and process the message.

Configuring the Middleware

With the use of C# extension methods, first and third party logic can be added to the MiddlewareBuilder. The builder extension methods can be influenced by parameters, Dependency Injection and other sources of information.

Also, helper methods can be added to simplify the creation of middleware.

// Typical third part integration
public static class MiddlewareBuilderExtensions
{
    public static IMiddlewareBuilder<TContext> UseLog(this IMiddlewareBuilder<TContext> self, bool verboseLogging = false)
    {
        // this block is run when the builder (extension) methods are invoked (one-time).
        // .. allows to perform configuration and collect metadata (e.g. via parameters or DI)
        return self.Use(LogFactory);

        MiddlewareDelegate<Context> LogFactory(MiddlewareDelegate<Context> next)
        {
            // this block is run when the middleware stack is actually build (one-time).
            // .. the shape of the middleware is known at this moment
            // .. has closure of configuration
            return LogAsync;

            async Task LogAsync(Context context)
            {
                // this block runs when the middleware stack is invoked (each-request)
                // .. has closure of configuration
                // .. has closure of shape

                // as a middleware it encapsulates the next from the building time
                if (verboseLogging) { Log.Write("Before"); }
                await next(context);
                if (verboseLogging) { Log.Write("After"); }
            }
        }
    }
}

// simple helper functions
public static partial class IMiddlewareBuilderExtensions
{
    public static IMiddlewareBuilder<TContext> Use<TContext>(this IMiddlewareBuilder<TContext> self, MiddlewareDelegate<TContext> middleware) where TContext : Context
        => self.Use(next => async context => { await middleware(context); await next(context); });
}

var messageHandler = new MiddlewareBuilder<Context>()
    .UseExceptionHandling()
    .UseLog(verbose: true)
    .Use(HandleAsync)
    .Build();
    
var message = GetMessage();
await messageHandler(message); // try-catch, log and process the message.

Conclusions

This article hopefully gave an introduction into how the violetgrass/middleware and ASP.NET Core middleware stack work. While sometimes it looks over-engineered, it is actually quite simple and a powerful extensibility framework.

There is more to typically middleware scenarios like dispatching and endpoint routing, however, this is material for other articles.