Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
StipoR opened this issue Apr 4, 2023 · 2 comments

Comments

@StipoR
Copy link

StipoR commented Apr 4, 2023

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
@Gymbotan
Copy link

I have the very similar problem, but with interface IDigest.
I created 6 libraries as separated projects (Standard 2.0, Standard 2.1, .Net Core 3.1, .Net Core 5.0, .Net Core 6.0, .Net Core 7.0) with the same implementation of IDigest interface. Then I created console app .Net Core 7.0 and added project reference to each of them (one link at a time). Console app worked only with 6.0 and 7.0 libraries. With others it throwed exception
IDigest exception

Then I created console app .Net Core 5.0 and it worked well at least with Standard 2.0 and .Net Core 5.0 libraries.

When I used Portable.BouncyCastle 1.9.0 I didn't have such a problem.

@fdub
Copy link

fdub commented Nov 6, 2023

This is also the case for a program targeting net6.0 that references a project targeting netstandard2.1 that in turn references BouncyCastle.Cryptography, although the preprocessor directive states that with NETSTANDARD2_1_OR_GREATER the void BlockUpdate(ReadOnlySpan<byte> input) overload would be included. The reason is that the BouncyCastle.Cryptography package only includes DLLs for net6.0 and netstandard2.0 but not netstandard2.1. Therefore the netstandard2.1 project uses the BouncyCastle netstandard2.0 library without the overload.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants