Skip to content

Adding a member to an interface breaks compatibility between netstandard2.0 and net6.0 #447

@StipoR

Description

@StipoR

Description

When using a project or a package that targets netstandard2.0, references BouncyCastle.Cryptography package, and implements some interfaces from BouncyCastle.Cryptography, such as ISigner, in a project that targets net6.0, System.TypeLoadException is thrown with the message: Method does not have an implementation.

Reproduction Steps

Visual Studio solution that reproduces the issue: BouncyCastleCryptographyInterfaceIssue.zip

ClassLibrary1.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BouncyCastle.Cryptography" Version="2.1.1" />
  </ItemGroup>

</Project>

ClassLibrary1.MySigner.cs:

using Org.BouncyCastle.Crypto;

namespace ClassLibrary1
{
    public class MySigner : ISigner
    {
        public string AlgorithmName => "MyAlgorithmName";

        public void BlockUpdate(byte[] input, int inOff, int inLen)
        {
        }

        public byte[] GenerateSignature()
        {
            return new byte[0];
        }

        public int GetMaxSignatureSize()
        {
            return 0;
        }

        public void Init(bool forSigning, ICipherParameters parameters)
        {
        }

        public void Reset()
        {
        }

        public void Update(byte input)
        {
        }

        public bool VerifySignature(byte[] signature)
        {
            return false;
        }
    }
}

ConsoleApp1.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" />
  </ItemGroup>

</Project>

ConsoleApp1.Program.cs:

using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main()
        {
            var mySignerAlgorithmName = GetMySignerAlgorithmName();

            Console.WriteLine(mySignerAlgorithmName);
        }

        private static string GetMySignerAlgorithmName()
        {
            return new ClassLibrary1.MySigner().AlgorithmName;
        }
    }
}

Expected behavior

MyAlgorithmName is output to the Console.

Actual behavior

An exception is thrown:

System.TypeLoadException
  HResult=0x80131522
  Message=Method 'BlockUpdate' in type 'ClassLibrary1.MySigner' from assembly 'ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
  Source=ConsoleApp1
  StackTrace:
   at ConsoleApp1.Program.GetMySignerAlgorithmName() in C:\Users\Stipo\Documents\Visual Studio 2022\Projects\BouncyCastleCryptographyInterfaceIssue\ConsoleApp1\Program.cs:line 17
   at ConsoleApp1.Program.Main() in C:\Users\Stipo\Documents\Visual Studio 2022\Projects\BouncyCastleCryptographyInterfaceIssue\ConsoleApp1\Program.cs:line 9

Regression?

Probably yes, after the member Org.BouncyCastle.Crypto.ISigner.BlockUpdate(ReadOnlySpan input) was added and after other conditional compilation members were added to other interfaces.

Known Workarounds

  • A project or package that references BouncyCastle.Cryptography package should target both netstandard2.0 and net6.0, but that is not possible if a package is coming from a third party.
  • Force usage of BouncyCastle.Cryptography.dll for netstandard2.0 by adding the following to the ConsoleApp1.csproj:
  <ItemGroup>
    <PackageReference Include="BouncyCastle.Cryptography" Version="2.1.1" ExcludeAssets="Compile" GeneratePathProperty="true" />
    <Reference Include="BouncyCastle.Cryptography">
      <HintPath>$(PkgBouncyCastle_Cryptography)\lib\netstandard2.0\BouncyCastle.Cryptography.dll</HintPath>
    </Reference>
  </ItemGroup>

But this is ugly because users of a third party package must take care of its BouncyCastle.Cryptography package dependency although they are not using the BouncyCastle.Cryptography package directly in their code.

Other information

The issue is happening because there are several interfaces in BouncyCastle.Cryptography package (such as Org.BouncyCastle.Crypto.ISigner that add a member conditionally when targeting net6.0, thus changing the interface contract from the compatible netstandard2.0 targeted framework.

The Change rules for compatibility state:

DISALLOWED: Adding a member to an interface
If you provide an implementation, adding a new member to an existing interface won't necessarily result in compile failures in downstream assemblies. However, not all languages support default interface members (DIMs). Also, in some scenarios, the runtime can't decide which default interface member to invoke. For these reasons, adding a member to an existing interface is considered a breaking change.

The solution is to either remove the problematic interface members (conditionally added ones, such as Org.BouncyCastle.Crypto.ISigner.BlockUpdate(ReadOnlySpan input)) (this would be a breaking change of BouncyCastle.Cryptography API) or provide a default implementation for them as explained in the Tutorial: Update interfaces with default interface methods.
For example, the fix for the Org.BouncyCastle.Crypto.ISigner.BlockUpdate(ReadOnlySpan input) would be:

#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
        /// <summary>Update the signer with a span of bytes.</summary>
        /// <param name="input">the span containing the data.</param>
        public void BlockUpdate(ReadOnlySpan<byte> input) => this.BlockUpdate(input.ToArray(), 0, input.Length);
#endif

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions