phmonte / Buildalyzer

A utility to perform design-time builds of .NET projects without having to think too hard about it.
MIT License
605 stars 95 forks source link

Blazor wasm projects not including .razor files #200

Closed groogiam closed 2 years ago

groogiam commented 2 years ago

I'm attempting to generate an adhoc workspace for a .NET 6 Blazor wasm project but Buildalyzer does not seem to pickup the razor files or their generated code. I'm not sure if this is a bug or something that is not supported. It seems like something that would be a useful feature. Thanks.

daveaglick commented 2 years ago

I'm going to guess there's a lot of magic involved in the codegen for Blazor WASM, though I haven't looked too closely at it. If so, I'm not surprised we're not seeing everything. Note that Buildalyzer primarily looks for the source files that get sent to csc so that's another reason it might not be picking up on generated Razor code.

In any case, I'm not sure whether to call this a bug, or just unsupported at this point. Do you have a simple reproduceable use case I can look at?

groogiam commented 2 years ago

Repro is attached. The source generation does not really have anything to do with wasm. CS files are generated from the razor files and compiled into the assembly. The mono clr running in wasm then interprets them at runtime, at least in development mode. AOT may be done on a publish but should not be triggered for a normal debug build.

I believe with Blazor 5 or 6 the code generation was switched to use source generators. This can be switched off in the project file (I have commented out the property to do this). Even with this switched off and the source files being generated to disk in obj they are still not picked up. I also added a razor class library project which shows the same issue. Again the razor class library should just be doing a straight compile on the generated cs files. This shows the same results as the BlazorWasmTest.

Let me know if you have any other questions on the Blazor side of things. I'm not an expert but I think I have a decent understanding of it. Thanks for taking the time to look at this.

BuildalyzerRepro200.zip

jonmiller1 commented 2 years ago

I'm seeing the same thing. All the other files are picked up except the razor files. Seems like this would break any project that uses razor files.

daveaglick commented 2 years ago

I finally got a chance to run through the repro you attached, thanks a lot for that - it makes this much clearer!

I think the issue here (if it can even be called an issue) is what the expectation is. It sounds like you were expecting Buildalyzer to see and report on the generated C# files that the Razor/Blazor compiler emits as part of the build and caching process. That's likely out of scope for Buildalyzer the same way looking at the generated source from .cshtml Razor files would be.

Supporting such a scenario would require some pretty significant changes under the hood. What Buildalyzer does is run MSBuild out of process while injecting a custom logger that sends logging events from MSBuild back to Buildalyzer for capture. In particular, Buildalyzer looks for an MSBuild task called Csc (or Fsc or Vbc to support F# and VB). That task is what calls csc.exe within MSBuild to compile the application and it contains all the source files passed to the C# compiler. That set of files as represented by command-line arguments to csc.exe is what Buildalyzer actually reports on.

Let's look at the RazorClassLibraryTest project with and without the source generators. Generating a binlog and viewing it in Structured Log Viewer with the default behavior shows that the file is passed into csc.exe wholesale, .razor extension and all:

image

That's surprising to me and suggests that the Blazor code generator must know how to read a .razor file and process it internally into content for compilation without resorting to an intermediate .cs file on disk. Or at a minimum, if there is an intermediate code-generated .cs file it's not passed into csc.exe but rather produced by the compiler as part of compilation. Buildalyzer doesn't have any insight into that process (at least not yet).

So when using the default Blazor code generation method, Blazor doesn't ever see the generated code because MSBuild doesn't ever see the generated code.

If we change the project file to include <UseRazorSourceGenerator>false</UseRazorSourceGenerator> only the RazorClassLibraryTest.GlobalUsings.g.cs file is passed to csc.exe via the Csc task:

image

And that lines up with what Buildalyzer sees and reports:

image

Which raises the question: what happened to the Component1.razor file since it's not being passed to csc.exe and there's no g.cs generated source file for it being passed to csc.exe either?

To answer that let's go back to the binlog and search for it. I found it being used in a RazorGenerateComponentDeclaration target with a SdkRazorGenerate task:

image

That seems to suggest that there's a rzc.dll that ships with the .NET SDK that Blazor uses for code generation when the default Roslyn code generation feature isn't being used. It looks like it gets passed pairs of input and output files and then emits them to disk. Those emitted code generated files don't appear anywhere else in the MSBuild log, so somehow they're making into the compilation, I just don't know how. And once again, if MSBuild doesn't see it, neither does Buildalyzer. I suspect that one of these analyzers being passed to csc.exe might be involved, but have no way of knowing for sure without a lot more research:

image

So where does that leave us? It looks like in both scenarios, with and without Roslyn code generators, the compiler (or more likely, an extension to the compiler like an analyzer) is taking the Blazor code and adding it to the compilation. That makes the process outside the scope of what Buildalyzer can see. We could potentially expose these additionalfile arguments on the IAnalyzerResult object from Buildalyzer, but that's about as far as I can go:

image

Would that be helpful? To be honest, even if we could find the generated source files for the Razor components, and assuming they even get written to disk somewhere (which I'm not certain about), I'm not sure that passing them into the Roslyn Workspace you get by calling manager.GetWorkspace() would work anyway. It seems like Blazor is instrumenting the compilation process in ways beyond what a Roslyn Workspace can do in the first place.

groogiam commented 2 years ago

I do find it very interesting that the compiler does not directly get passed any of the files in the case of <UseRazorSourceGenerator>false</UseRazorSourceGenerator> being present. This setting outputs the files to obj. E.g. BlazorWasmTest\obj\Debug\net6.0\Razor\Pages and RazorClassLibraryTest\obj\Debug\net6.0\Razor image Not sure if this opens up a new avenue for investigation or not.

In any case I think adding the additional file entries with the .razor files would be helpful. Knowing they are part of the project without having to start looking around the file system is half the battle. After doing some research it looks like it is possible to do some processing to generate the blazor cs files using Microsoft.AspNetCore.Razor.Language package. There may also be some option to have the Roslyn Workspace which comes from Buildalyzer.Workspaces generate them by hooking into the source generator system.

Thanks for again for looking into this and providing such a detailed explanation of what is going on.

daveaglick commented 2 years ago

I've added IAnalyzerResult.AdditionalFiles and the release is rolling out momentarily. I'll go ahead and close this one since that's probably the best we can do at the moment. If you think of another way to handle this that's still in-scope for Buildalyzer and MSBuild, or have other ideas, we can always re-open it or add another issue.

Thanks for sticking with me as I worked through this one!

groogiam commented 2 years ago

Sounds good. I really appreciate your help. Thanks.