StackExchange / StackExchange.Redis

General purpose redis client
https://stackexchange.github.io/StackExchange.Redis/
Other
5.85k stars 1.5k forks source link

System.MissingMethodException method not found: 'Void StackExchange.Redis.ConfigurationOptions.set_EndPoints(StackExchange.Redis.EndPointCollection)' #2619

Closed astryia closed 6 months ago

astryia commented 6 months ago

Description

I have netstandard2.0 project which is wrapper around StackExchange.Redis nuget. I have net8.0 console project which is using netstandard2.0 one. I'm getting the following runtime error:

"Method not found: 'Void StackExchange.Redis.ConfigurationOptions.set_EndPoints(StackExchange.Redis.EndPointCollection)'."}
    ClassName: null
    Data: {System.Collections.ListDictionaryInternal}
    HResult: -2146233069
    HasBeenThrown: true
    HelpLink: null
    InnerException: null
    MemberName: null
    Message: "Method not found: 'Void StackExchange.Redis.ConfigurationOptions.set_EndPoints(StackExchange.Redis.EndPointCollection)'."
    SerializationStackTraceString: "   at CacheWrapper.CacheWrapper.BuildConfigurationOptions()\r\n   at CacheWrapper.CacheWrapper..ctor() in CacheWrapper.cs:line 11\r\n   at Program.<Main>$(String[] args) in Program.cs:line 3"
    SerializationWatsonBuckets: null
    Signature: null
    Source: "CacheWrapper"

As I understand that the problem is in StackExchange.Redis nuget package which uses init only setters in netstandard version of the library.

Reproduction Steps

CacheWrapper.csproj

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

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

  <ItemGroup>
    <PackageReference Include="StackExchange.Redis" Version="2.7.10" />
  </ItemGroup>

</Project>

CacheWrapper.cs

using StackExchange.Redis;

namespace CacheWrapper
{
    public class CacheWrapper
    {
        private readonly ConfigurationOptions _options;

        public CacheWrapper()
        {
            _options = BuildConfigurationOptions();
        }

        public IDatabase Connect()
        {
            var connectionMultiplexer = ConnectionMultiplexer.Connect(_options);
            return connectionMultiplexer.GetDatabase();
        }

        internal ConfigurationOptions BuildConfigurationOptions()
        {
            var endPoints = new EndPointCollection { { "127.0.0.1", 11111 } };

            return
                new ConfigurationOptions
                {
                    AbortOnConnectFail = false,
                    EndPoints = endPoints,
                    SyncTimeout = 30000
                };
        }
    }
}

CacheUser.csproj

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

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

</Project>

Program.cs

var cache = new CacheWrapper.CacheWrapper();

This code works if my console application is targeting net48.

mgravell commented 6 months ago

I'll see if I can repro; however, as a pragmatic workaround for now: maybe just done use the setter, but rather: either use a collection initializer expression, or just access .Endpoints manually and use .Add etc

mgravell commented 6 months ago

In other news: I'm wondering if we should mark this setter as [Obsolete] - and add a separate mechanism. It seems to be causing all manner of pain

astryia commented 6 months ago

I'll see if I can repro; however, as a pragmatic workaround for now: maybe just done use the setter, but rather: either use a collection initializer expression, or just access .Endpoints manually and use .Add etc

You are right, manually initializing collection is a possible workaround.

Another workaround will be making Wrapper project multitarget, so the dependencies sequence will be console-app net8.0 -> wrapper-lib net8.0 -> StackExchange.Redis net6.0 instead of console-app net8.0 -> wrapper-lib netstandard2.0 -> StackExchange.Redis net6.0

But all such workarounds are not intuitive.

I think something should be done on the library level, for example getting rid of this hack

See difference in IL code of the wrapper project: net8.0

    IL_0025: dup
    IL_0026: ldloc.0      // endPoints
    IL_0027: callvirt     instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) [StackExchange.Redis]StackExchange.Redis.ConfigurationOptions::set_EndPoints(class [StackExchange.Redis]StackExchange.Redis.EndPointCollection)
    IL_002c: nop

netstandard2.0

    IL_0025: dup
    IL_0026: ldloc.0      // endPoints
    IL_0027: callvirt     instance void modreq ([StackExchange.Redis]System.Runtime.CompilerServices.IsExternalInit) [StackExchange.Redis]StackExchange.Redis.ConfigurationOptions::set_EndPoints(class [StackExchange.Redis]StackExchange.Redis.EndPointCollection)
    IL_002c: nop
WeihanLi commented 6 months ago

Related issue: https://github.com/dotnet/runtime/issues/96197

mgravell commented 6 months ago

Thanks. I'll repro, add the type--forward, and see if that works. Interesting.

Note: we can't simply get rid of "[that] hack" without is being a breaking change; regrets are cheap, sadly. Resolutions: more expensive.