The C# language has turned on the turbo boosters regarding language features, with folks either loving or hating the additions. It’s fair to have an opinion, but today I’d like to show you one of my favorite new features coming to the C# language and why you should care. Static Abstract members in interfaces is mind-blowing from its immediate usage in your projects and the implications it can have for framework authors and project maintainers. So come with me on this journey, and while you see these examples, be sure to think about your use cases. Then, let’s get into it.

Note: You’ll need .NET 6+ and to have your LangVersion set to preview in your web application’s csproj file. This feature was introduced in C# 11 (.NET 7), but was available to .NET 6 SDK users under preview language features. I previously said it was available in C# 10, which was technically incorrect.

What Are Static Abstract Members

C# developers are familiar with the interface declaration, but let me explain for those who are not. An interface is a mechanism in which you can define a contract. Classes whole implement an interface must, in one way or another, fulfill the contract. Interfaces can include methods, properties, indexers, and events. For the longest time, You only declared interfaces with no implementation. In C# 8, the language introduced us to static members, allowing us to share base functionality across all interface implementors. The language feature significantly reduced the implementations needed in your codebase, especially if your codebase implemented an interface a lot.

Static abstract members allow each implementing member of an interface to implement their version of a static accessor that you can access via the Type handle. You can implement these members implicitly or explicitly, like any other interface definition. Let’s take a look at an example, as it makes more apparent how it all works.

void HasSeeds<T>(T fruit) where T: IFruit {
    Console.WriteLine(T.HasSeeds);
}

HasSeeds(new Apple());
HasSeeds(new Watermelon());

public record Watermelon : IFruit
{
    public static bool HasSeeds => false;
}

public record Apple : IFruit
{
    public static bool HasSeeds => true;
}

public interface IFruit
{
    static abstract bool HasSeeds { get; }
}

Notice how you can access the HasSeeds static member in our generic method without knowing exactly the specific Type of T. Typically this kind of access would only be available through reflection. As is commonly known, reflection can be a performance killer and a source of runtime exceptions. As you may have guessed already, we don’t have those issues here. Static abstract members are also an excellent method for adding general metadata about our types while enforcing each implementor completes the contract.

A Possible Use Case For Static Abstract Members

Most ASP.NET Core frameworks operate primarily on reflection and finding metadata through Attribute types. However, reflection can add substantial overhead to start-up times and can be error-prone, as developers may forget to decorate endpoints with the correct attributes. Let’s look at an example of an MVC endpoint directly from the ASP.NET Core templates.

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5)
            .Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
    }
}

As you may notice, there are at least there pieces of metadata on this one controller: HttpGet, Route, and ApiController. Unfortunately, ASP.NET Core must also scan our project and use reflection to determine the presence of these attributes. Additionally, there is no way to enforce that a user uses HttpGet or Route. Forgetting to add these attributes can lead to hours of frustrating debugging with your application exhibiting behaviors like unreachable endpoints, not being included in the OpenAPI specification, or failing to generate links internally.

There’s been some exploration in the .NET Community around providing an endpoint-based web programming model. However, even those approached currently have to resort to reflection with metadata. So let’s see if we can build a more strict approach without reflection, all the while pushing our users into the PIT OF SUCCESS!.

using static Microsoft.AspNetCore.Http.Results;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapHandler<HelloWorld>();
app.Run();

public interface IHandler
{
    static abstract string Template { get; }
    static abstract HttpMethod Method { get; }
    static abstract Delegate Handle { get; }
}

public record struct HelloWorld : IHandler
{
    public static HttpMethod Method => HttpMethod.Get;
    public static string Template => "/";
    public static Delegate Handle => 
        (HttpRequest _) => Ok("Hello, World!");
}

public static class ApplicationHandlerExtensions
{
    public static void MapHandler<THandler>(this WebApplication app)
        where THandler : IHandler
    {
        app.MapMethods(
            THandler.Template, 
            new[] {THandler.Method.ToString() }, 
            THandler.Handle );
    }
}

We’ll see the expected “Hello, World” from our Handle method by running the application. It’s not a far stretch to imagine building a Source Generator that retrieves all implementations of IHandler and registers the endpoints, possibly all through a single invocation of MapHandlers. Again, no reflection, all compile-time-based registration of our endpoints based on very strict interfaces.

We can see the most significant advantages in the MapHandler method, where we use the THandler handle to access the static members.

public static class ApplicationHandlerExtensions
{
    public static void MapHandler<THandler>(this WebApplication app)
        where THandler : IHandler
    {
        app.MapMethods(
            THandler.Template, 
            new[] {THandler.Method.ToString() }, 
            THandler.Handle );
    }
}

Conclusion

Static abstract members allow us to give agency to implementors of a contract to do what they want while giving us the ability to extract metadata that previously has only been possible through reflection or code generation. I hope this post has sparked some interesting thoughts about the possibilities in your codebase. If you’d like to share them, please follow me on Twitter at @buhakmeh. I’d love to hear them. Thank you for reading, and best of luck on your .NET journeys.

References