-
Notifications
You must be signed in to change notification settings - Fork 580
Description
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
andnet6.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