Skip to content
This repository has been archived by the owner on Nov 7, 2018. It is now read-only.

Backwards Compatibility Broken with IOptionsSnapshot when upgrading to .Net Core 2.0 #228

Closed
Aurel opened this issue Aug 31, 2017 · 5 comments

Comments

@Aurel
Copy link

Aurel commented Aug 31, 2017

I've run into a bit of a strange issue and I was wondering if this is actually indicative of a larger issue. I've boiled down the problem into the simplest, smallest set of code that still reproduces the issue itself, so here it is.

There seems to be a clash when invoking .Value on an IOptionsSnapshot object if the objects are targeting different .Net versions. It only reproduces in certain cases, and the error message is extremely strange.

The Class Library (netstandard1.3)

The set up is as follows. I have a .Net Standard 1.3 class library. It has a reference to Microsoft.Extensions.Options.ConfigurationExtensions (1.1.2). For the sake of example, I've boiled down the code to this. Here is the entire content of this class library:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

public static class OptionsExtensions
{
	public static void AddSnapshotConfig<T>(this IServiceCollection services, IConfiguration configuration)
		where T : class, new()
	{
		services.Configure<T>(configuration);
		services.AddScoped(config => config.GetService<IOptionsSnapshot<T>>().Value);
	}
}

Now, what this does, is configure an IOptionsSnapshot, but then actually scope the object inside of snapshot so it's accessible directly from DI without having to ask for the IOptions or IOptionsSnapshot itself. This has worked just fine in the past, and I've got this set of code in a class library a number of my own projects are using.

The Web App

Now, let's create a NetCore2.0app project in the same solution, with the default API interface. This means we get program.cs, startup.cs and controller/ValuesController.cs, as the template creates it.

Let's not change program.cs, but let's make the Startup file look like this:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public class ConfigurationOptions
{
	public string Whatever { get; set; }
}

public class SomeScopedService
{
	public SomeScopedService(ConfigurationOptions options) {}
}

public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	public IConfiguration Configuration { get; }

	public void ConfigureServices(IServiceCollection services)
	{
		services.AddSnapshotConfig<ConfigurationOptions>(Configuration);
		services.AddScoped<SomeScopedService>();
		services.AddMvc();
	}

	public void Configure(IApplicationBuilder app, IHostingEnvironment env)
	{
		app.UseDeveloperExceptionPage();
		app.UseMvc();
	}
}

Lastly, let's make the default ValuesController request this custom SomeScopedService.

Here's a simplified version of that file:

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

[Route("api/[controller]")]
public class ValuesController : Controller
{
	public ValuesController(SomeScopedService service) { }

	[HttpGet]
	public IEnumerable<string> Get()
	{
		return new string[] { "Some Text", "Other Text" };
	}
}

Clearly we're not actually doing anything with the ScomeScopedService or with the ConfigurationOptions but, we're requesting them from DI.

Just in case it's relevant, the web is only referencing:

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
  </ItemGroup>

The Result

When we hit run and actually navigate to the api/values endpoint, we get the following error message:

MissingMethodException: Method not found: 'System.__Canon Microsoft.Extensions.Options.IOptionsSnapshot`1.get_Value()'.

The call stack is something like:

System.MissingMethodException: Method not found: 'System.__Canon Microsoft.Extensions.Options.IOptionsSnapshot`1.get_Value()'.
   at OptionsExtensions.<>c__0`1.<AddSnapshotConfig>b__0_0(IServiceProvider config)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.<>c__DisplayClass22_0.<RealizeService>b__0(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.Internal.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
   at lambda_method(Closure , IServiceProvider , Object[] )

What's happening, or so it seems - is that, it seems to be running into a bizarre conflict between the Microsoft.Extension.Options from the 1.1.2 in the class library, and the 2.0.0 reference in the Web App.

While this might seem a bit contrived for the sake of this example, this is just a simplified repro - in reality, I have a much more complicated system that's basically doing this - registering some options in some custom way, and I have tons of code using the options in custom services directly. The moment I upgrade any of those projects to .net Core 2.0, the webapps fail to run.

Why might this be? I know that the .Value was removed and moved up to the IOptions itself, but I think something is going on very strangely here.

@HaoK
Copy link
Member

HaoK commented Aug 31, 2017

We changed the implementation/behavior of IOptionsSnapshot entirely in 2.0, we basically repurposed the type name as its still meant to be used to capture the state of an options instance for the lifetime of a request, instead of having a lifetime timed to the IOptionsMonitor

I can help you migrate to 2.0 if you get stuck, IOptionsMonitor is still mostly the same in 2.0. Were you using snapshot for getting updating/reloading options?

@Aurel
Copy link
Author

Aurel commented Sep 3, 2017

Hey @HaoK - I'm quite well aware that the IOptionsSnapshot has changed, but in the process of changing, did the backwards compatibility break? Essentially, my old class libraries that I have just don't work with NetCore 2.0 apps. Let's say that I need my class libraries to remain as < 2.0 NetStandard apps.

Yes, originally, I was using snapshot for reloading options on the fly. Is that not what it does anymore? In the case, the documentation is a bit out of date and should reference that. Does this also mean that my old class library that has all of this functionality can only work with older versions of .net core, and not with newer ones?

I've come up with a number of fixes, and I got it working, but the fixes are hacky, and force me to use dynamic everywhere so I can try calling .Value, and if that throws I can try calling .Get(string.Empty), even though I'm in a NetStandard1.3 app that doesn't have that function. It's one way I've found of solving this problem. However, I'm just trying to report that something goes horribly wrong here.

Additionally, it only happens if the options are requested by a Service. If I just request the options themselves, not much is happening, but if I request a service, and in its constructor requests the POCO object I showed above, I consistently get this error about MissingMethodException

@HaoK
Copy link
Member

HaoK commented Sep 5, 2017

@Aurel yes, the 2.0 IOptionsSnapshot type is not backwards compatible with 1.0 at all

@Aurel
Copy link
Author

Aurel commented Sep 9, 2017

Interesting - I've found a fix locally that uses a bunch of dynamics so I don't have to know about the type I'm invoking, but can you explain to me then, why this might be happening? I don't understand why the get_Value() method would be missing, when it's clearly there both in 1.1.2 and in 2.0.0! (In 2.0.0 it's inherited from IOption<T>)

@aspnet-hello
Copy link

This issue was moved to dotnet/aspnetcore#2387

@aspnet aspnet locked and limited conversation to collaborators Jan 1, 2018
@aspnet-hello aspnet-hello removed this from the Discussions milestone Jan 1, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants