Versioning

Prior to C# 8.0, when creating a new version of a component or application that other developers have programmed against, you should not change interfaces. Because interfaces define a contract between the implementing class and the class using the interface, changing the interface is equivalent to changing the contract, which will possibly break any code written against the interface.

Changing or removing an interface member signature is obviously a code-breaking change, as any call to that member will no longer compile without modification. The same is true when you change public or protected member signatures on a class. However, unlike with classes, adding members to an interface could also prevent code from compiling without additional changes. The problem is that any class implementing the interface must do so entirely, and implementations for all members must be provided. With new interface members, the compiler will require that developers add new interface members to the class implementing the interface.

With C# 8.0, the “don’t change interfaces” rule changes slightly. C# 8.0 added a mechanism for enabling a default implementation for an interface member, such that adding a member (you still can’t remove or modify an existing member in a version-compatible way) will not trigger compiler errors on all implementations. Prior to C# 8.0, there is a way to achieve a similar result to changing an interface by adding an additional interface. In this section, we discuss both approaches.

Guidelines
DO NOT add members without a default implementation to a published interface.
Interface Versioning Prior to C# 8.0

The creation of IDistributedSettingsProvider in Listing 8.11 serves as a good example of extending an interface in a version-compatible way. Imagine that initially only the ISettingsProvider interface is defined (as it was in Listing 8.6). In the next version, however, it is determined that settings could be distributed to multiple resources (URIs8) (perhaps on a per-machine basis). To enable this constraint, the IDistributedSettingsProvider interface is created; it derives from ISettingsProvider.

Listing 8.11: Deriving One Interface from Another
interface IDistributedSettingsProvider : ISettingsProvider
{
    /// <summary>
    /// Get the settings for a particular URI
    /// </summary>
    /// <param name="uri">
    /// The URI name the setting is related to</param>
    /// <param name="name">The name of the setting</param>
    /// <param name="defaultValue">
    /// The value returned if the setting is not found</param>
    /// <returns>The specified setting</returns>
    string GetSetting(
        string uri, string name, string defaultValue);
 
    /// <summary>
    /// Set the settings for a particular URI
    /// </summary>
    /// <param name="uri">
    /// The URI name the setting is related to</param>
    /// <param name="name">The name of the setting</param>
    /// <param name="value">The value to be persisted</param>
    /// <returns>The specified setting</returns>
    void SetSetting(
        string uri, string name, string value);
}

The important issue is that programmers with classes that implement ISettingsProvider can choose to upgrade the implementation to include IDistributedSettingsProvider, or they can ignore it.

If, instead of creating a new interface, the URI-related methods are added to ISettingsProvider, classes implementing this interface will potentially throw an exception at runtime and certainly will not successfully compile with the new interface definition. In other words, changing ISettingsProvider is a version-breaking change, both at the binary level and at the source code level.

Changing interfaces during the development phase is obviously acceptable, although perhaps laborious if implemented extensively. However, once an interface is published, it should not be changed. Instead, a second interface should be created, possibly deriving from the original interface. (Listing 8.11 includes XML comments describing the interface members, as discussed further in Chapter 10.)

Interface Versioning in C# 8.0 or Later

Until now, we have ignored the new C# 8.0 interface features except to mention that they exist. In this section, we abandon that restriction and describe the C# 8.0 feature set known as default interface members. As described earlier, changing a published interface in any way prior to C# 8.0 will break any code that implements the interface; therefore, published interfaces should not be changed. However, starting with C# 8.0, Microsoft introduced a new C# language feature that allows interfaces to have members with implementation—that is, concrete members, not just declarations. Consider, for example, the CellColors property included in Listing 8.12.

Listing 8.12: Versioning Interfaces with Default Interface Members
public interface IListable
{
    // Return the value of each cell in the row
    string?[] CellValues
    {
        get;
    }
 
    ConsoleColor[] CellColors
    {
        get
        {
            var result = new ConsoleColor[CellValues.Length];
            // Using generic Array method to populate array
            // (see Chapter 12)
            Array.Fill(result, DefaultColumnColor);
            return result;
        }
    }
    static public ConsoleColor DefaultColumnColor { getset; }
}
 
// ...
 
public class Contact : PdaItem, IListable
{
    // ...
 
    #region IListable
    string[] IListable.CellValues
    {
        get
        {
            return new string[]
            {
                FirstName,
                LastName,
                Phone,
                Address
            };
        }
    }
    // *** No CellColors implementation *** //
    #endregion IListable
 
    // ...
}
 
public class Publication : IListable
{
    // ...
 
    #region IListable
    string?[] IListable.CellValues
    {
        get
        {
            return new string[]
            {
                Title,
                Author,
                Year.ToString()
            };
        }
    }
 
    ConsoleColor[] IListable.CellColors
    {
        get
        {
            string?[] columns = ((IListable)this).CellValues;
            ConsoleColor[] result = ((IListable)this).CellColors;
            if (columns[YearIndex]?.Length != 4)
            {
                result[YearIndex] = ConsoleColor.Red;
            }
            return result;
        }
    }
    #endregion IListable
    // ...
}

In this listing, notice the addition of the CellColors property getter. As you can see, it includes an implementation even though it is the member of an interface. The feature is called a default interface member because it provides a default implementation of the method so that any class that implements the interface will already have a default implementation—so that code will continue to compile without any changes even though the interface has additional members. The Contact class, for example, has no implementation for the CellColors property getter, so it relies on the default implementation provided by the IListable interface.

Not surprisingly, you can override a default implementation of the method in the implementing class to provide a different behavior that makes more sense to the class. This behavior is all consistent with the purpose of enabling polymorphism as outlined at the beginning of the chapter.

However, the default interface member feature includes additional features. The primary purpose of these features is to support refactoring of default interface members (though some would debate this interpretation). To use them for any other purpose likely indicates a flaw in the code structure, because it implies the interface is used for more than polymorphism. Table 8.1 lists the additional language constructs along with some of their important limitations.

Table 8.1: Default Interface Refactoring Features

C# 8.0–Introduced Interface Construct

Sample Code

Static Members

The ability to define static members on the interface including fields, constructors, and methods. (This includes support for defining a static Main method—an entry point into your program.) The default accessibility for static members on interfaces is public.

public interface ISampleInterface

{

   private static string? _Field;

   public static string? Field

   {

       get => _Field;

       private set => _Field = value; }

       static IsampleInterface() =>

            Field = "Nelson Mandela";

     public static string? GetField() => Field;

}

Implemented Instance Properties and Methods

You can define implemented properties and members on interfaces. Since instance fields are not supported, properties cannot work against backing fields. Also, without instance fields support, there is no automatically implemented property support.

Note that to access a default implemented property, it is necessary to cast to the interface containing the member. The class (Person) does not have the default interface member available unless it is implemented.

public interface IPerson

{

   // Standard abstract property definitions

   string FirstName { get; set; }

   string LastName { get; set; }

   string MiddleName { get; set; }

   // Implemented instance properties and methods

   public string Name => GetName();

   public string GetName() =>

       $"{FirstName} {LastName}";

}

public class Person

{

   // ...

}

public class Program

{

   public static void Main()

   {

       Person inigo = new Person("Inigo", "Montoya");

       Console.Write(

           ((IPerson)inigo).Name);

   }

}

public Access Modifier

The default for all instance interface members. Use this keyword to help clarify the accessibility of the code. Note, however, that the compiler-generated CIL code is identical with or without the public access modifier.

public interface IPerson

{

   // All members are public by default

   string FirstName { get; set; }

   public string LastName { get; set; }

   string Initials =>

       $"{FirstName[0]}{LastName[0]}";

   public string Name => GetName();

   public string GetName() =>

       $"{FirstName} {LastName}";

}

protected Access Modifier

Accessible only from members in classes that derive from the defining class..

public interface IPerson

{

   // ...

   protected void Initialize() =>

   { /* ... */ };

}

private Access Modifier

The private access modifier restricts a member to be available for invocation only from the interface that declares it. It is designed to support refactoring of default interface members. All private members must include an implementation.

public interface IPerson

{

   string FirstName { get; set; }

   string LastName { get; set; }

   string Name => GetName();

   private string GetName() =>

       $"{FirstName} {LastName}";

}

internal Access Modifier

internal members are only visible from within the same assembly in which they are declared.

public interface IPerson

{

   string FirstName { get; set; }

   string LastName { get; set; }

   string Name => GetName();

   internal string GetName() =>

       $"{FirstName} {LastName}";

}

private protected Access Modifier

A super set of private and protected; private protected members are visible from within the same assembly and from within other interfaces that derive from the containing interface. Like protected members, classes external to the assembly cannot see protected internal members.

public interface IPerson

{

   string FirstName { get; set; }

   string LastName { get; set; }

   string Name => GetName();

   protected internal string GetName() =>

       $"{FirstName} {LastName}";

}

private protected Access Modifier

Accessing a private protected member is only possible from the containing interface or interfaces that derive from the implementing interface. Even classes implanting the interface cannot access a private protected member, as demonstrated by the PersonTitle property in Person.

class Program

{

  static void Main()

   {

       IPerson? person = null;

       // Non-deriving classes cannot call

       // private protected member.

       // _ = person?.GetName();

       Console.WriteLine(person);

   }

}

public interface IPerson

{

   string FirstName { get; }

   string LastName { get; }

   string Name => GetName();

   private protected string GetName() =>

       $"{FirstName} {LastName}";

}

public interface IEmployee: IPerson

{

   int EmpoyeeId => GetName().GetHashCode();

}

public class Person : IPerson

{

   public Person(

       string firstName, string lastName)

   {

       FirstName = firstName ??

           throw new ArgumentNullException(nameof(firstName));

       LastName = lastName ??

           throw new ArgumentNullException(nameof(lastName));

   }

   public string FirstName { get; }

   public string LastName { get; }

   // private protected interface members

   // are not accessible in derived classes.

   // public int PersonTitle =>

   // GetName().ToUpper();

}

virtual Modifier

By default, an implemented interface member is virtual, meaning that derived implementations of the method with the same signature will be invoked when the interface member is invoked. As with the public access modifier, however, you can decorate a member as virtual explicitly to provide clarity. For non-implemented interface members, virtual is not allowed. Similarly, virtual is incompatible with private, static, and sealed modifiers.

public interface IPerson

{

   // virtual is not allowed on members

   // without implementation

   /* virtual */ string FirstName { get; set; }

   string LastName { get; set; }

   virtual string Name => GetName();

   private string GetName() =>

       $"{FirstName} {LastName}";

}

sealed Modifier

To prevent a derived class from overriding a method, mark it as sealed, thus ensuring that the method implementation cannot be modified by derived classes.

See Listing 8.13 for more information.

public interface IWorkflowActivity

{

   // Private and, therefore, not virtual

   private void Start() =>

       Console.WriteLine(

           "IWorkflowActivity.Start()...");

   // Sealed to prevent overriding

   sealed void Run()

   {

       try

       {

           Start();

           InternalRun();

       }

       finally

       {

           Stop();

       }

   }

   protected void InternalRun();

   // Private and, therefore, not virtual

   private void Stop() =>

       Console.WriteLine(

           "IWorkflowActivity.Stop()..");

}

abstract Modifier

The abstract modifier is only allowable on members without an implementation, but the keyword has no effect as such members are abstract by default.

All abstract members are automatically virtual and explicitly declaring abstract members as virtual triggers a compile error.

public interface IPerson

{

   // virtual is not allowed on members

   // without implementation

   /* virtual */ abstract string FirstName

     { get; set; }

   string LastName { get; set; }

   // abstract is not allowed on members

   // with implementation

   /* abstract */ string Name => GetName();

   private string GetName() =>

       $"{FirstName} {LastName}";

}

Partial Interfaces and Partial Methods

It is now possible to provide partial implementations of a method with no outgoing data (returns or ref/out data) and optionally the fully implemented method in a second declaration of the same interface.

Partial methods are always private—they do not support access modifiers

public partial interface IThing

{

   string Value { get; protected set; }

   void SetValue(string value)

   {

       AssertValueIsValid(value);

       Value = value;

   }

   partial void AssertValueIsValid(string value);

}

public partial interface IThing

{

   partial void AssertValueIsValid(string value)

   {

       // Throw if value is invalid.

       switch(value)

       {

           case null:

               throw new ArgumentNullException(

                   nameof(value));

           case "":

               throw new ArgumentException(

                   "Empty string is invalid",

                   nameof(value));

           case string _ when

               string.IsNullOrWhiteSpace(value):

               throw new ArgumentException(

                   "Can't be whitespace",

                   nameof(value));

       };

   }

}

There are a couple of points to highlight in Table 8.1. First, it is important to note that automatically implemented property support is not available because instance fields (which back an automatically implemented property) are not supported. This is a significant difference from abstract classes, which do support instance fields and automatically implemented properties.

Second, notice static members on interfaces are also public by default. While static members always have an implementation and map closely to class static members in this regard, in contrast, the purpose of interface instance members is to support polymorphism, so they default to public.

Additional Encapsulation and Polymorphism with Protected Interface Members

When creating a class, programmers should be careful about choosing to allow overriding of a method, since they cannot control the derived implementation. Virtual methods should not include critical code because such methods may never be called if the derived class overrides them.

Listing 8.13 includes a virtual Run() method. If the WorkflowActivity programmer calls Run() with the expectation that the critical Start() and Stop() methods will be called, then the Run() method may fail.

Listing 8.13: Carelessly Relying on a Virtual Method Implementation
public class WorkflowActivity
{
    private static void Start()
    {
        // Critical code
    }
    public virtual void Run()
    {
        Start();
        // Do something...
        Stop();
    }
    private static void Stop()
    {
        // Critical code
    }
}

In overriding Run(), a developer could perhaps not call the critical Start() and Stop() methods.

Now consider a fully implemented version of this scenario with the following encapsulation requirements:

It should not be possible to override Run().
It should not be possible to invoke Start() or Stop(), as the order in which they execute is entirely under the control of the containing type (which we will name IWorkflowActivity).
It should be possible to replace whatever executes in the “Do something …” code block.
If it were reasonable to override Start() and Stop(), then the class implementing them should not necessarily be able to invoke them—they are part of the base implementation.
The deriving types should be allowed to provide a Run() method, but it should not be invoked when the Run() method is invoked on IWorkflowActivity.

To meet all these requirements and more, C# 8.0 provides support for a protected interface member, which has some significant differences from a protected member on a class. Listing 8.14 demonstrates the differences, and Output 8.3 shows the results.

Listing 8.14: Forcing the Desirable Run() Encapsulation
public interface IWorkflowActivity
{
    // Private and, therefore, not virtual
    private static void Start() =>
        Console.WriteLine(
            "IWorkflowActivity.Start()...");
 
    // Sealed to prevent overriding.
    sealed void Run()
    {
        try
        {
            Start();
            InternalRun();
        }
        finally
        {
            Stop();
        }
    }
 
    protected void InternalRun();
 
    // Private and, therefore, not virtual
    private static void Stop() =>
        Console.WriteLine(
            "IWorkflowActivity.Stop()...");
}
 
public interface IExecuteProcessActivity : IWorkflowActivity
{
    protected void RedirectStandardInOut() =>
        Console.WriteLine(
            "IExecuteProcessActivity.RedirectStandardInOut()...");
 
    // Sealed not allowed when overriding.
    /* sealed */
    void IWorkflowActivity.InternalRun()
    {
        RedirectStandardInOut();
        ExecuteProcess();
        RestoreStandardInOut();
    }
    protected void ExecuteProcess();
    protected void RestoreStandardInOut() =>
        Console.WriteLine(
            "IExecuteProcessActivity.RestoreStandardInOut()...");
}
 
public class ExecuteProcessActivity : IExecuteProcessActivity
{
    public ExecuteProcessActivity(string executablePath) =>
        ExecutableName = executablePath
            ?? throw new ArgumentNullException(nameof(executablePath));
 
    public string ExecutableName { get; }
 
    void IExecuteProcessActivity.RedirectStandardInOut() =>
        Console.WriteLine(
            "ExecuteProcessActivity.RedirectStandardInOut()...");
 
    void IExecuteProcessActivity.ExecuteProcess() =>
        Console.WriteLine(
            $"ExecuteProcessActivity.IExecuteProcessActivity.ExecuteProcess()...");
 
    public static void Run()
    {
        ExecuteProcessActivity activity = new("dotnet");
        // Protected members cannot be invoked
        // by the implementing class even when
        //  implemented in the class.
        // ((IWorkflowActivity)this).InternalRun();
        //  activity.RedirectStandardInOut();
        //  activity.ExecuteProcess();
        Console.WriteLine(
            @$"Executing non-polymorphic Run() with process '{
                activity.ExecutableName}'.");
    }
}
 
public class Program
{
    static public void Main()
    {
        ExecuteProcessActivity activity = new("dotnet");
 
        Console.WriteLine(
            "Invoking ((IExecuteProcessActivity)activity).Run()...");
        // Output:
        // Invoking ((IExecuteProcessActivity)activity).Run()...
        // IWorkflowActivity.Start()...
        // ExecuteProcessActivity.RedirectStandardInOut()...
        // ExecuteProcessActivity.IExecuteProcessActivity.
        // ExecuteProcess()...
        // IExecuteProcessActivity.RestoreStandardInOut()...
        // IWorkflowActivity.Stop()..
        ((IExecuteProcessActivity)activity).Run();
 
        // Output:
        // Invoking activity.Run()...
        // Executing non-polymorphic Run() with process 'dotnet'.
        Console.WriteLine();
        Console.WriteLine(
            "Invoking activity.Run()...");
        ExecuteProcessActivity.Run();
    }
}
Output 8.3
Invoking ((IExecuteProcessActivity)activity).Run()...
IWorkflowActivity.Start()...
ExecuteProcessActivity.RedirectStandardInOut()...
ExecuteProcessActivity.IExecuteProcessActivity.ExecutProcess()...
IExecuteProcessActivity.RestoreStandardInOut()...
IWorkflowActivity.Stop()..
Invoking activity.Run()...
Executing non-polymorphic Run() with process 'dotnet'.

Let’s consider how Listing 8.14 meets the requirements outlined earlier.

Notice that IWorkflowActivity.Run() is sealed and, therefore, not virtual. This prevents any derived types from changing its implementation. Any invocation of Run(), given a IWorkflowActivity type, will always execute the IWorkflowActivity implementation.

IWorkflowActivity’s Start() and Stop() methods are private, so they are invisible to all other types. Even though IExecutProcessActivity seemingly has start/stop-type activities, IWorkflowActivity doesn’t allow for replacing its implementations.

IWorkflowActivity defines a protected InternalRun() method that allows IExecuteProcessActivity (and ExecuteProcessActivity, if desirable) to overload it. However, notice that no member of ExecuteProcessActivity can invoke InternalRun(). Perhaps that method should never be run out of sequence from Start() and Stop(), so only an interface (IWorkflowActivity or IExecuteProcessActivity) in the hierarchy is allowed to invoke the protected member.

All interface members that are protected can override any default interface member if they do so explicitly. For example, both the RedirectStandardInOut() and RestoreStandardInOut() implementations on ExecuteProcessActivity are prefixed with IExecuteProcessActivity. And, like with the protected InternalRun() method, the type implementing the interface cannot invoke the protected members; for example, ExecuteProcessActivity can’t invoke RedirectStandardInOut() and RestoreStandardInOut(), even though they are implemented on the same type.

Even though only one of them is explicitly declared as virtual, both RedirectStandardInOut() and RestoreStandardInOut() are virtual (virtual is the default unless a member is sealed). As such, the most derived implementation will be invoked. Therefore, when IExecuteProcessActivity.InternalRun() invokes RedirectStandardInOut(), the implementation on ExecuteProcessActivity() will execute instead of the implementation from IExecuteProcessActivity.

A derived type’s implementation can potentially provide a method that matches a sealed signature in the parent. For example, if ExecuteProcessActivity provides a Run() method that matches the signature of Run() in IWorkflowActivity, the implementation associated with the type will execute, rather than the most derived implementation. In other words, Program.Main()’s invocation of ((IExecuteProcessActivity)activity).Run() calls IExecuteProcessActivity.Run(), while activity.Run() calls ExecuteProessActivity.Run()—where activity is of type ExecuteProcessActivity.

In summary, the encapsulation available with protected interface members, along with the other member modifiers, provides a comprehensive mechanism for encapsulation—albeit an admittedly complicated one.

________________________________________

8. Universal resource identifiers.
{{ snackbarMessage }}
;