aspnetcore: Fatal regression: 6.0.0-preview2 and later break linking for SignalR client

Describe the bug

When Microsoft.AspNetCore.SignalR.Client is updated from 6.0.0-preview1 to preview4 (and possibly preview2 and 3) it no longer works in a Xamarin.Forms (iOS) project if the linker is enabled.

6.6.0-preview1 and earlier versions work fine even when the linker is enabled.

To Reproduce

  1. Install Microsoft.AspNetCore.SignalR.Client 6.0.0-preview4 in a Xamarin.Forms/iOS project. Note: If you upgrade or downgrade the library, make sure to close down VS For Mac and delete all bin and obj folders as the changes don’t seem to be reflected in the build if this is not done.
  2. Use the “HubConnectionBuilder” to connect to the SignalR backend
  3. Turn on linking (“Link SDKs only”)
  4. Build and run
  5. The app crashes when it’s trying to call the HubConnectionBuilder in the SignalR client library, with the error “System.InvalidOperationException: A suitable constructor for type 'Microsoft.Extensions.Options.UnnamedOptionsManager'1[Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions]' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.”.

The crash only happens if linking is enabled and the version is 6.0.0-preview4 (I verified that preview1 works, am unsure exactly where it got broken). The full stack trace is as follows:

System.InvalidOperationException: A suitable constructor for type 'Microsoft.Extensions.Options.UnnamedOptionsManager'1[Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions]' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite (Microsoft.Extensions.DependencyInjection.ServiceLookup.ResultCache lifetime, System.Type serviceType, System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00021] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateOpenGeneric (Microsoft.Extensions.DependencyInjection.ServiceDescriptor descriptor, System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain, System.Int32 slot, System.Boolean throwOnConstraintViolation) [0x0004a] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateOpenGeneric (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00025] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x0003a] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00010] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites (System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain, System.Reflection.ParameterInfo[] parameters, System.Boolean throwIfCallSiteNotFound) [0x00016] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite (Microsoft.Extensions.DependencyInjection.ServiceLookup.ResultCache lifetime, System.Type serviceType, System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00050] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (Microsoft.Extensions.DependencyInjection.ServiceDescriptor descriptor, System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain, System.Int32 slot) [0x00073] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00018] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x0002e] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00010] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor (System.Type serviceType) [0x0000c] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at System.Collections.Concurrent.ConcurrentDictionary'2[TKey,TValue].GetOrAdd (TKey key, System.Func'2[T,TResult] valueFactory) [0x00034] in <cf60a21f6a4543e5a30e3c6ae6742e37>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope serviceProviderEngineScope) [0x00013] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType) [0x00008] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService (System.Type serviceType) [0x00000] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T] (System.IServiceProvider provider) [0x0000e] in <44acc522f4fc4e959fecc254d3efdbfc>:0
  at Microsoft.AspNetCore.SignalR.Client.HubConnectionBuilder.Build () [0x00025] in <207ee6dcc63548f6a7de0cf3bd6154d5>:0
  at MyProject.Services.ServerConnectionService.CreateConnection () [0x0000c] in /Users/tommikiviniemi/Projects/myproject/MyApp/Services/ServerConnectionService.cs:133
...

The code it complains about, HubConnectionBuilder.Build, will crash even with an empty connection builder:

return new HubConnectionBuilder().Build();

Please note that this is a Xamarin.Forms project, in case it has any bearing on anything.

It goes without saying that this is a catastrophic failure and I’d really appreciate a fix. Thank you.

Exceptions (if any)

System.InvalidOperationException: A suitable constructor for type 'Microsoft.Extensions.Options.UnnamedOptionsManager'1[Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions]' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite (Microsoft.Extensions.DependencyInjection.ServiceLookup.ResultCache lifetime, System.Type serviceType, System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00021] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateOpenGeneric (Microsoft.Extensions.DependencyInjection.ServiceDescriptor descriptor, System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain, System.Int32 slot, System.Boolean throwOnConstraintViolation) [0x0004a] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateOpenGeneric (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00025] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x0003a] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00010] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites (System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain, System.Reflection.ParameterInfo[] parameters, System.Boolean throwIfCallSiteNotFound) [0x00016] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite (Microsoft.Extensions.DependencyInjection.ServiceLookup.ResultCache lifetime, System.Type serviceType, System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00050] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (Microsoft.Extensions.DependencyInjection.ServiceDescriptor descriptor, System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain, System.Int32 slot) [0x00073] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00018] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x0002e] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00010] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor (System.Type serviceType) [0x0000c] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at System.Collections.Concurrent.ConcurrentDictionary'2[TKey,TValue].GetOrAdd (TKey key, System.Func'2[T,TResult] valueFactory) [0x00034] in <cf60a21f6a4543e5a30e3c6ae6742e37>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope serviceProviderEngineScope) [0x00013] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType) [0x00008] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService (System.Type serviceType) [0x00000] in <5c5c2d1e15f743cd9b15d3422c0b5a91>:0
  at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T] (System.IServiceProvider provider) [0x0000e] in <44acc522f4fc4e959fecc254d3efdbfc>:0
  at Microsoft.AspNetCore.SignalR.Client.HubConnectionBuilder.Build () [0x00025] in <207ee6dcc63548f6a7de0cf3bd6154d5>:0
  at MyProject.Services.ServerConnectionService.CreateConnection () [0x0000c] in /Users/tommikiviniemi/Projects/myproject/MyApp/Services/ServerConnectionService.cs:133
...

Further technical details

=== Visual Studio Community 2019 for Mac ===

Version 8.10 (build 1773)
Installation UUID: c397ff94-0681-4a22-9972-062a757ab635
	GTK+ 2.24.23 (Raleigh theme)
	Xamarin.Mac 6.18.0.23 (d16-6 / 088c73638)

	Package version: 612000140

=== Mono Framework MDK ===

Runtime:
	Mono 6.12.0.140 (2020-02/51d876a041e) (64-bit)
	Package version: 612000140

=== Roslyn (Language Service) ===

3.10.0-3.21251.8+4c32f5e4e9c0828a94fd4d1c9c0994082c85aaf3

=== NuGet ===

Version: 5.9.0.7134

=== .NET Core SDK ===

SDK: /usr/local/share/dotnet/sdk/5.0.203/Sdks
SDK Versions:
	5.0.203
	5.0.202
	3.1.409
	3.1.408
MSBuild SDKs: /Applications/Visual Studio.app/Contents/Resources/lib/monodevelop/bin/MSBuild/Current/bin/Sdks

=== .NET Core Runtime ===

Runtime: /usr/local/share/dotnet/dotnet
Runtime Versions:
	5.0.6
	5.0.5
	3.1.15
	3.1.14

=== .NET Core 3.1 SDK ===

SDK: 3.1.409

=== Xamarin.Profiler ===

Version: 1.6.15.68
Location: /Applications/Xamarin Profiler.app/Contents/MacOS/Xamarin Profiler

=== Updater ===

Version: 11

=== Apple Developer Tools ===

Xcode 12.5 (18205)
Build 12E262

=== Xamarin.Mac ===

Xamarin.Mac not installed. Can't find /Library/Frameworks/Xamarin.Mac.framework/Versions/Current/Version.

=== Xamarin.iOS ===

Version: 14.20.0.1 (Visual Studio Community)
Hash: fe0e2c518
Branch: d16-10
Build date: 2021-05-19 08:15:56-0400

=== Xamarin.Android ===

Version: 11.3.0.1 (Visual Studio Community)
Commit: xamarin-android/d16-10/22fc2b3
Android SDK: /Users/tommikiviniemi/Library/Developer/Xamarin/android-sdk-macosx
	Supported Android versions:
		None installed

SDK Tools Version: 26.1.1
SDK Platform Tools Version: 30.0.4
SDK Build Tools Version: 30.0.2

Build Information: 
Mono: b4a3858
Java.Interop: xamarin/java.interop/d16-10@f39db25
ProGuard: Guardsquare/proguard/v7.0.1@912d149
SQLite: xamarin/sqlite/3.35.4@85460d3
Xamarin.Android Tools: xamarin/xamarin-android-tools/d16-10@c5732a0

=== Microsoft OpenJDK for Mobile ===

Java SDK: /Users/tommikiviniemi/Library/Developer/Xamarin/jdk/microsoft_dist_openjdk_1.8.0.25
1.8.0-25
Android Designer EPL code available here:
https://github.com/xamarin/AndroidDesigner.EPL

=== Android SDK Manager ===

Version: 16.10.0.12
Hash: e240b8c
Branch: remotes/origin/d16-10
Build date: 2021-05-13 17:01:38 UTC

=== Android Device Manager ===

Version: 16.10.0.14
Hash: e340248
Branch: remotes/origin/d16-10
Build date: 2021-05-13 17:01:56 UTC

=== Xamarin Designer ===

Version: 16.10.0.117
Hash: 249267d55
Branch: remotes/origin/d16-10
Build date: 2021-05-24 21:27:04 UTC

=== Build Information ===

Release ID: 810001773
Git revision: 56d63e5691f86f863cfaed823a5a8fe430e1aaa9
Build date: 2021-05-28 11:21:28-04
Build branch: release-8.10

=== Operating System ===

Mac OS X 10.16.0
Darwin 20.5.0 Darwin Kernel Version 20.5.0
    Sat May  8 05:10:33 PDT 2021
    root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Comments: 32 (13 by maintainers)

Commits related to this issue

Most upvoted comments

Ok, I finally got my mac all updated and working so I could test this out and I can now say I understand why this is happening.

Between 6.0-preview1 and 6.0-preview2, we added the [AssemblyMetadata("IsTrimmable", "true"] attributes to all the assemblies shipping from dotnet/runtime in https://github.com/dotnet/runtime/pull/48428.

It appears that the Xamarin iOS mtouch tool, which contains a version of the ILLinker code, is now starting to respect that assembly attribute as a way of telling if the assembly should be trimmed or not. See https://github.com/xamarin/xamarin-macios/pull/11229.

If I look at the mtouch.exe on my machine in ILSpy I see the following code:

protected override bool IsLinkerSafeAttribute(CustomAttribute attribute)
{
	TypeReference attributeType = attribute.get_AttributeType();
	string name = ((MemberReference)attributeType).get_Name();
	string text = name;
	if (!(text == "LinkerSafeAttribute"))
	{
		if (text == "AssemblyMetadataAttribute")
		{
			if (!attribute.get_HasConstructorArguments())
			{
				return false;
			}
			if (attributeType.get_Namespace() != "System.Reflection")
			{
				return false;
			}
			CustomAttributeArgument val = attribute.get_ConstructorArguments().get_Item(0);
			if (((CustomAttributeArgument)(ref val)).get_Value() as string!= "IsTrimmable")
			{
				return false;
			}
			val = attribute.get_ConstructorArguments().get_Item(1);
			return ((CustomAttributeArgument)(ref val)).get_Value().ToString().ToLowerInvariant() == "true";
		}
		return false;
	}
	return true;
}

This logic is used to tell whether to set the AssemblyAction.Link action on the assembly or not.

However, the issue is, the iOS mtouch tool should only be respecting the IsTrimmable assembly attribute if it also respects all the other attributes we’ve added to the new version of the linker: DynamicallyAccessedMembers, DynamicDependency, etc. Just because an assembly has IsTrimmable on it, doesn’t mean that it can be trimmed without also respecting these new attributes.

I believe this issue should be moved to https://github.com/xamarin/xamarin-macios, as the current version of the mtouch tool is not doing the right thing.

cc @spouliot @sbomer

That remains a problem if some of the dotnet/runtime assemblies do not plan to support being trimmable for net6.

We have backtracked on a few assemblies that we don’t plan on supporting trimmability. See https://github.com/dotnet/runtime/pull/52272 and https://github.com/dotnet/runtime/pull/49085. The current list of libraries that don’t plan on supporting trimming in the near future:

  • System.Composition.Convention
  • System.ComponentModel.Composition.Registration
  • System.Composition.TypedParts
  • System.Composition.Hosting
  • System.ComponentModel.Composition
  • System.CodeDom
  • System.Configuration.ConfigurationManager
  • System.Speech

For those assemblies we don’t add the IsTrimmable assembly metadata - https://github.com/dotnet/runtime/search?q=SetIsTrimmable

But the rest of the assemblies we ship in the .NET 6 timeframe will have IsTrimmable=true on them. So anyone using our v6 OOBs on the current Xamarin stack will run into this issue.

It was the exact same problem that builds with the linker enabled (“Link SDKs only”) would remove the default constructor, and they fixed it in the above commit. Maybe @pictos could chime in with details?

I believe that @spouliot is aware of that workaround, I learned it from him here (; I’m reading the role thread to have context, but looks like an interesting issue… AFAIK net6 doesn’t the Mono’s linker (Xamarin.Forms needs mono to build) and mono doesn’t know .NET6’s linker, so that could be an issue? (I’m guessing here)

Here’s what we can do to clear things up:

  • SignalR hasn’t done any work to make it work well with a linker in any scenario. If it worked before you got lucky, or something else was rooting code that made it work. This is similar to unity workloads that break left and right with IL linkers.
  • .NET 5 introduced a new way for libraries to annotate their code to influence the linker and we’ve used this in a couple of places in .NET and ASP.NET Core to make it more linker friendly.
  • .NET 6 does much more of this annotating and fixing things so that we can be more confident running the linker will work or will warn when its going to break.

As for the current release of Xamarin.Forms, I don’t know how it worked before or why it worked out of the box. I assume something in your code (or in a hidden linker file or extensibility point) was rooting these types. It’s nothing that was influenced by SignalR itself.

UnnamedOptionsManager is a new internal type that was introduced in .NET 6 preview4 but it’s an implementation detail. The old type used in its place was OptionsManager. It’s possible that OptionsManager was rooted by something else (we need to figure out what that was) and that UnnamedOptionsManager isn’t rooted.

This is the crux of the issue.

As for which linker tech is being used, it seems like we’re talking about the linker before it understood these annotations.