.NET Tools How-To's

Static Interface Members, Generic Attributes, Auto-Default Structs – Using C# 11 in Rider and ReSharper

ReSharper and Rider support for C# 11

The .NET 7 SDK arrived a few months ago, with many .NET developers looking forward to this release and the brand-new C# language features that come along with it. If you haven’t put your fingers on it yet, all you need is:

  • Download the latest .NET SDK
  • Update your global.json if you have one
  • Update the TargetFramework in your project to net7.0 (only for some features)
  • Update the LangVersion property in your project to 11.0 or preview

In this series, we will dive into the most interesting features that are coming with C# 11 and show how we updated ReSharper and Rider to support you in applying them to your codebase with ease:

This fourth post will walk you through the static interface members, generic attributes, and auto-default structs.

Static Interface Members

Once extension methods were brought in with C# 3, developers could easily extend existing interfaces and their inheritors to provide additional functionality without introducing breaking changes (i.e., without requiring inheritors to implement new members):

interface IMyInterface { }

static class Extensions
{
    public static void Method(this IMyInterface obj) { /* ... */ }
}

With C# 8, the language team added default interface members, and the approach to extending an interface slightly improved. Now you could add a static constructor for the interface, define real properties (as opposed to Get<Property> methods), and allow inheritors to override the default implementation with something more specific. Again – without making breaking changes:

interface IMyInterface
{
    static IMyInterface() { /* ... */ }

    public string Property => "default";
}

class Implementation : IMyInterface
{
    public string Property => "overridden";
}

In C# 11, the introduction of static virtual members once again lifts the restrictions. While extension methods and the first wave of default interface members only allowed adding instance-like members (working in static context though), this latest improvement allows you to add static members as well. Those members can be declared with or without a default implementation:

interface IMyInterface
{
    static abstract string Foo { get; }    // without implementation
    static virtual string Bar => "value";  // with implementation
}

The definition of abstract members in interfaces was particularly motivated by the generic math feature, which works on the idea of defining constants and operators for numeric types inheriting from INumber<T> or similar abstractions, thus allowing them to be used with general mathematical algorithms:

public interface INumber<TSelf> : ... { }

public interface IAdditionOperators<TSelf, TOther, TResult>
{
    static abstract TResult operator +(TSelf left, TOther right);
}

public interface IAdditiveIdentity<TSelf, TResult>
{
    static abstract TResult AdditiveIdentity { get; }
}

Static interface members can also be used to reduce allocations as Nick Chapses shows in one of his videos. Instead of having to new T() an interface implementation, you can call the static member directly:

void AccessPropertyThroughStatic<T>() where T : IWithStaticVirtual
    => Console.WriteLine(T.Text);

void AccessPropertyThroughInstance<T>() where T : IWithoutStaticAbstract, new()
    => Console.WriteLine(new T().Text);

ReSharper and Rider make it easy to work with static interface members. If a type does not implement a static abstract member yet, you’ll see the usual error with a quick-fix to create the declaration:

Implementing missing Static Member

When you want to override the default implementation of a member, you can call the Generate Code action and choose Overriding Members:

Overriding a Static Virtual Member

Maybe you’ve already spotted a tiny little detail we’ve added. A member that has a default implementation will show a little plus indicator on the usual gutter mark hierarchy icon:

Default Implementatin Indicator in Gutter Mark Icon

Generic Attributes

Attributes couldn’t define generic parameters up until C# 10. The lack of generic attributes has been frustrating for many .NET developers. Maybe even surprisingly, this does not just apply to the attribute usage but also to intermediate type declarations:

// Forbidden prior to C# 11
class MyAttribute : MyAttributeBase<OtherType> { }
class MyAttributeBase<T> : Attribute { }

// Definitely forbidden prior to C# 11
[MyAttribute<OtherType>]
class MyClass { }

For most developers, the workaround has been to pass the type via typeof, either as a constructor or named argument. Unfortunately, this is rather verbose and also prevents you from using generic constraints for better static analysis:

class SerializationAttribute : Attribute
{
    public Type Type { get; set; }
}

[Serialization(Type = typeof(JsonSerializer))]
class Person { }

In C# 11, this restriction has been lifted and you can now use generic types as with any other type:

class SerializationAttribute<T> : Attribute
    where T : ISerializer
{
}

[Serialization<JsonSerializer>]
class Person { }

ReSharper and Rider are lifting this restriction as well and all existing features should work as expected with them.

Auto-Default Structs

Before C# 11, you were required to initialize all members in the constructor of a struct before you could call any instance methods or return from the constructor:

struct Credentials
{
    public string? Username;
    public string? Password;

    public Credentials(string username, string password)
    {
        Username = username;
        Password = password;
    }

    public Credentials(string token)
    {
        // Previously required:
        // Username = null;
        Password = token;
    }
}

In C# 11, you no longer need to initialize all members in the constructor of a struct. Members will use the default value if not set, just as with classes. This makes it easier to change classes to structs and vice versa.

ReSharper and Rider understand auto-default structs and will no longer show the old warning whenever you’re targeting the latest language version. Furthermore, we updated our Generate | Constructor action to omit the this() constructor initializer when possible:

struct Credentials
{
    public string? Username;
    public string? Password;

    public Credentials(string password)
        // Only generated when necessary
        // : this()
    {
        Password = password;
    }
}

Conclusion

Static interface members break the old rule of interfaces only being able to define instance members. They are particularly useful for defining custom numeric types, but can also help to reduce allocations throughout your codebase. Generic attributes lift a similar limitation and you no longer have to use typeof anymore. Auto-default structs remove some of the boilerplate code of initializing members with default values.

Let us know in the comments if you have more ideas for additional features in ReSharper and Rider. We hope you liked this series of diving into C# 11 features and that you’ve learned how ReSharper and Rider can help with these!

image description