statiqdev / Statiq.Framework

A flexible and extensible static content generation framework for .NET.
https://statiq.dev/framework
MIT License
425 stars 74 forks source link

"Object does not match target type" error on dotnet 6 rc1 on linux and macOS #204

Closed phil-scott-78 closed 3 years ago

phil-scott-78 commented 3 years ago

Seeing this error compiling razor on dotnet 6 rc1 but only with Linux and macOS - Object does not match target type

Preview releases of dotnet 6 worked fine, but with the latest version this is failing on cshtml.

You can see a full build output here but here is the full stack trace.

[DBUG] Exception while executing pipeline Archives/PostProcess: System.Reflection.TargetException: Object does not match target type.
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at Statiq.Razor.StatiqViewCompiler.CreateCompilation(String generatedCode, String assemblyName)
   at Statiq.Razor.StatiqViewCompiler.CompileAndEmit(RazorCodeDocument codeDocument, String generatedCode)
   at Statiq.Razor.RazorCompiler.GetCompilation(RazorProjectItem projectItem)
   at Statiq.Razor.RazorCompiler.<>c__DisplayClass11_0.<CompilePage>b__0(CompilerCacheKey _)
   at Statiq.Common.ConcurrentCache`2.<>c__DisplayClass2_1.<GetOrAdd>b__1()
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at Statiq.Razor.CachingCompiler.GetOrAddCachedCompilation(CompilerCacheKey cacheKey, Func`2 valueFactory)
   at Statiq.Razor.RazorCompiler.CompilePage(RenderRequest request, Int32 contentCacheCode, RazorProjectItem projectItem)
   at Statiq.Razor.RazorCompiler.GetPageFromStreamAsync(IServiceProvider serviceProvider, RenderRequest request)
   at System.Lazy`1.CreateValue()
   at Statiq.Common.ConcurrentCache`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Statiq.Razor.CachingCompiler.GetOrAddCachedCompilation(CompilerCacheKey cacheKey, Func`2 valueFactory)
   at Statiq.Razor.RazorCompiler.CompilePage(RenderRequest request, Int32 contentCacheCode, RazorProjectItem projectItem)
   at Statiq.Razor.RazorCompiler.GetPageFromStreamAsync(IServiceProvider serviceProvider, RenderRequest request)
   at System.Lazy`1.CreateValue()
   at Statiq.Common.ConcurrentCache`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Statiq.Razor.CachingCompiler.GetOrAddCachedCompilation(CompilerCacheKey cacheKey, Func`2 valueFactory)
   at Statiq.Razor.RazorCompiler.CompilePage(RenderRequest request, Int32 contentCacheCode, RazorProjectItem projectItem)
   at Statiq.Razor.RazorCompiler.GetPageFromStreamAsync(IServiceProvider serviceProvider, RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
--- End of stack trace from previous location ---
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
--- End of stack trace from previous location ---
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
--- End of stack trace from previous location ---
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
--- End of stack trace from previous location ---
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
--- End of stack trace from previous location ---
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
--- End of stack trace from previous location ---
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
phil-scott-78 commented 3 years ago

To make sure it wasn't anything weird I was doing, because Lord knows I do weird things with Statiq, I built up the simplest reproduction I could. Sure enough ,same error. Reproduction is here - https://github.com/phil-scott-78/statiq-dotnet6

Got a bit less noise on this exception with the singular file when running dotnet run -l Debug

[ERRO] Content/PostProcess » RenderContentPostProcessTemplates » ExecuteIf » ExecuteIf » RenderRazor » [/Users/philscott/RiderProjects/StatiqNet6/input/index.cshtml => index.html] Object does not match target type.
[DBUG] Exception while executing pipeline Content/PostProcess: System.Reflection.TargetException: Object does not match target type.
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at Statiq.Razor.StatiqViewCompiler.CreateCompilation(String generatedCode, String assemblyName)
   at Statiq.Razor.StatiqViewCompiler.CompileAndEmit(RazorCodeDocument codeDocument, String generatedCode)
   at Statiq.Razor.RazorCompiler.GetCompilation(RazorProjectItem projectItem)
   at Statiq.Razor.RazorCompiler.<>c__DisplayClass11_0.<CompilePage>b__0(CompilerCacheKey _)
   at Statiq.Common.ConcurrentCache`2.<>c__DisplayClass2_1.<GetOrAdd>b__1()
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.get_Value()
   at Statiq.Common.ConcurrentCache`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Statiq.Razor.CachingCompiler.GetOrAddCachedCompilation(CompilerCacheKey cacheKey, Func`2 valueFactory)
   at Statiq.Razor.RazorCompiler.CompilePage(RenderRequest request, Int32 contentCacheCode, RazorProjectItem projectItem)
   at Statiq.Razor.RazorCompiler.GetPageFromStreamAsync(IServiceProvider serviceProvider, RenderRequest request)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RenderRazor.<>c__DisplayClass16_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
phil-scott-78 commented 3 years ago

Progress! On macOS I'm getting a Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler rather than the expected Microsoft.AspNetCore.Mvc.Razor.Compilation.RuntimeViewCompiler when calling InnerViewCompilerProvider.GetCompiler().

Not sure why or how to move forward, but that's the scoop so far.

phil-scott-78 commented 3 years ago

Sorry for the train of thought here, but I'm in the weeds and I wanted to dump what I found so hopefully someone smarter than me can pick up the trail, or at least tell me I'm on a wild goose chase here.

This change is sus, especially considering this comment "This name is hardcoded in RazorRuntimeCompilationMvcCoreBuilderExtensions. Make sure it's updated if this is ever renamed." Looks like with this commit they merged the DefaultViewCompiler and DefaultViewCompilerProvider into one class.

But tbh I'm struggling wrapping my head around whether this would cause my failure. With Statiq being compiled against 3.1 it feels like this conditional might not trigger with the new name causing the TryAddSingleton to skip the add because a previous instance never got removed. But that doesn't quite explain why it works in Windows and not macOS or Ubuntu. And I'm not sure why a different runtime would even cause this either.

daveaglick commented 3 years ago

Fantastic work getting to something we can investigate further. At first glance I think you’re definitely on to something and going down the right track. I’ll try to jump in and take a closer look tomorrow or in the next couple days. I do love a good debugging mystery :)

daveaglick commented 3 years ago

Going to move this issue to Framework since it's almost certainly with the Framework Razor extension.

Also for anyone else who ends up here with the same problem, a global.json locking .NET to 5 until this is resolved should help work around it (https://docs.microsoft.com/en-us/dotnet/core/versions/selection#the-sdk-uses-the-latest-installed-version):

{
  "sdk": {
    "version": "5.0.0"
  }
}
phil-scott-78 commented 3 years ago

Ok, had some more time to look at this tonight.

Because I was rushing I missed a key bit of detail - I had a global.json on my windows box that was forcing the beta version of dotnet-6. So this issue isn't OS specific which makes things SO much easier to wrap my head around. I stepped into the ASP.NET code and sure enough that change I pointed to seems to be at fault. I'm gonna try and get an easier reproduction going and maybe submit a PR to get this in before .NET 6 goes live...maybe

phil-scott-78 commented 3 years ago

Created an issue in the aspnetcore project - https://github.com/dotnet/aspnetcore/issues/37049

This is a rough one.

daveaglick commented 3 years ago

Fantastic work on the root cause here. I'm curious what the ASP.NET team says and if they'll fix this upstream now that it's at RC. If not, we might still have a couple options. We could consider registering our own DefaultViewCompiler - though I haven't groked the issue enough to know if that would help?

Going to try and find some time later today to jump in and understand this issue while it's fresh for you. Hopefully together we can figure out a path forward.

phil-scott-78 commented 3 years ago

This is definitely a nasty bug on their part. But the best I can tell it requires the Razor compilation to be referenced via a netcore 3.1 class library that is being used by a net6 application.

That enough for them to permanently require that DefaultViewCompileProvider class to be permanently the same name forever to keep that compatibility though?

Unfortunately for me to do the magic with generating the social cards I need net6. But you all might need to pin net5 rc long term until you can bring everything to net6. Net6 might have some very interesting hot reload options though...

phil-scott-78 commented 3 years ago

Forgot to add - I could be totally wrong on the severity because I still have no idea why the runtime is registering that class to begin with instead of the netcore3.1. Bit over my head when it comes to runtime magic

daveaglick commented 3 years ago

In RazorRuntimeCompilationMvcCoreBuilderExtensions.AddServices() (which is called by AddRazorRuntimeCompilation), it does this:

services.TryAddSingleton<IViewCompilerProvider, RuntimeViewCompilerProvider>();

That sets the RuntimeViewCompilerProvider as the servicec for IViewCompilerProvider, but there's a problem. It's using TryAdd... which will only bind the interface type to the implementation type if no other implementation type is already registered for that interface. The RazorRuntimeCompilationMvcCoreBuilderExtensions.AddServices() method tries to account for this by calling this just before the TryAdd...:

ServiceDescriptor serviceDescriptor = services.FirstOrDefault((ServiceDescriptor f) => f.ServiceType == typeof(IViewCompilerProvider) && f.ImplementationType?.Assembly == typeof(IViewCompilerProvider).Assembly && f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompilerProvider");
if (serviceDescriptor != null)
{
    services.Remove(serviceDescriptor);
}

In an ideal world, that would remove the existing implementation type for IViewCompilerProvider before adding the new runtime one. But as you noted, .NET 6 no longer registers DefaultViewCompilerProvider as the implementation of IViewCompilerProvider - instead it registers a newly combined DefaultViewCompiler which also implements IViewCompilerProvider. Therefore, the code above doesn't find the existing IViewCompilerProvider to remove it and following that, because there's still a registered implementation, doesn't register the RuntimeViewCompilerProvider we were expecting to see. And because Statiq assumes we'll get a RuntimeViewCompilerProvider when we ask for a IViewCompilerProvider, some other reflection code breaks due to mismatched types.

All of this is likely due to different library versions. Statiq currently uses the 3.1.x version of the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation libraries. I don't think I'm prepared to update Razor entirely yet (unless lots more problems on .NET 6 become evident), so let's take that as a given. On the other hand, some more foundational ASP.NET Core libraries ride with the SDK, so that's where the disconnect is. The RuntimeCompilation library thinks it's 3.1.x ASP.NET Core code so that check above should work. But .NET 6 moved the cheese and now they're incompatible.

So...the solution! It comes down to removing that IViewCompilerProvider from the service collection before registering (or rather letting it register) the RuntimeViewCompilerProvider one we're expecting. We can be pretty indiscrimanant here since we're not playing is as big a sandbox - removing any other IViewCompilerProvider besides the one we know we're about to register should be sufficient:

ServiceDescriptor serviceDescriptor = serviceCollection.FirstOrDefault((ServiceDescriptor f) => f.ServiceType == typeof(IViewCompilerProvider));
if (serviceDescriptor is object)
{
    serviceCollection.Remove(serviceDescriptor);
}

That allows the AddRazorRuntimeCompilation() call to do it's thing and we're good across .NET Core 3, .NET 5, and .NET 6. I'll push a commit shortly.

phil-scott-78 commented 3 years ago

Very clever! I like it.

Once a build is out there I'll pull it down and give it a go

daveaglick commented 3 years ago

Okay - fix is in, just wanted to make sure it wouldn't break on older .NET targets (though it's a pretty low-risk change given the existing behavior). I'll get a release out later this week - have a couple more things in flight so not quite ready to publish one yet.

phil-scott-78 commented 3 years ago

FWIW, I didn't notice any changes to this code in the dotnet repo, but just to be sure I checked RC2. No magic - still not working.

daveaglick commented 3 years ago

Meanwhile I’ve still got to get a release with the fix out. Somewhere along the way I broke an unrelated test and am still trying to track that down 😭

phil-scott-78 commented 3 years ago

Saw the latest release and I wanted to confirm it fixes this issue for anyone else that ends up here googling the error.

Thanks!