88 using System . Linq ;
99 using System . Net . Http ;
1010 using System . Net . Http . Json ;
11+ using System . Reflection . Metadata ;
1112 using System . Runtime ;
1213 using System . Text ;
1314 using System . Threading . Tasks ;
@@ -48,14 +49,21 @@ public class CompilationService
4849 private static CSharpCompilation _baseCompilation ;
4950 private static CSharpParseOptions _cSharpParseOptions ;
5051
52+ private int _lastCodeHash ;
53+ private CompileToAssemblyResult _cachedResult ;
54+
5155 private readonly RazorProjectFileSystem fileSystem = new VirtualRazorProjectFileSystem ( ) ;
5256 private readonly RazorConfiguration configuration = new (
5357 RazorLanguageVersion . Latest ,
5458 ConfigurationName : "Blazor" ,
5559 Extensions : ImmutableArray < RazorExtension > . Empty ) ;
5660
57- public static async Task InitAsync ( Func < IReadOnlyCollection < string > , ValueTask < IReadOnlyList < byte [ ] > > > getReferencedDllsBytesFunc )
61+ private RazorProjectEngine _declarationProjectEngine ;
62+
63+ public static unsafe Task InitAsync ( )
5864 {
65+ if ( _baseCompilation != null ) return Task . CompletedTask ;
66+
5967 var basicReferenceAssemblyRoots = new [ ]
6068 {
6169 typeof ( Console ) . Assembly , // System.Console
@@ -71,22 +79,32 @@ public static async Task InitAsync(Func<IReadOnlyCollection<string>, ValueTask<I
7179 typeof ( WebAssemblyHostBuilder ) . Assembly , // Microsoft.AspNetCore.Components.WebAssembly
7280 typeof ( FluentValidation . AbstractValidator < > ) . Assembly ,
7381 } ;
74- var assemblyNames = await getReferencedDllsBytesFunc ( basicReferenceAssemblyRoots
75- . SelectMany ( assembly => assembly . GetReferencedAssemblies ( ) . Concat (
76- [
77- assembly . GetName ( )
78- ] ) )
79- . Select ( assemblyName => assemblyName . Name )
80- . ToHashSet ( ) ) ;
81-
82- var basicReferenceAssemblies = assemblyNames
83- . Select ( peImage => MetadataReference . CreateFromImage ( peImage , MetadataReferenceProperties . Assembly ) )
84- . ToList ( ) ;
82+
83+ var assemblyNames = basicReferenceAssemblyRoots
84+ . SelectMany ( assembly => assembly . GetReferencedAssemblies ( ) . Concat ( [ assembly . GetName ( ) ] ) )
85+ . Select ( assemblyName => assemblyName . Name ! )
86+ . ToHashSet ( ) ;
87+
88+ // netstandard facade is needed for libraries that target netstandard2.0
89+ assemblyNames . Add ( "netstandard" ) ;
90+
91+ var loadedByName = AppDomain . CurrentDomain . GetAssemblies ( )
92+ . Where ( a => ! a . IsDynamic && a . GetName ( ) . Name != null )
93+ . ToDictionary ( a => a . GetName ( ) . Name ! , StringComparer . OrdinalIgnoreCase ) ;
94+
95+ var references = new List < MetadataReference > ( ) ;
96+ foreach ( var name in assemblyNames )
97+ {
98+ if ( ! loadedByName . TryGetValue ( name , out var assembly ) ) continue ;
99+ if ( ! assembly . TryGetRawMetadata ( out byte * blob , out int length ) ) continue ;
100+ var moduleMetadata = ModuleMetadata . CreateFromMetadata ( ( IntPtr ) blob , length ) ;
101+ references . Add ( AssemblyMetadata . Create ( moduleMetadata ) . GetReference ( ) ) ;
102+ }
85103
86104 _baseCompilation = CSharpCompilation . Create (
87105 DefaultRootNamespace ,
88106 Array . Empty < SyntaxTree > ( ) ,
89- basicReferenceAssemblies ,
107+ references ,
90108 new CSharpCompilationOptions (
91109 OutputKind . DynamicallyLinkedLibrary ,
92110 optimizationLevel : OptimizationLevel . Release ,
@@ -99,6 +117,7 @@ public static async Task InitAsync(Func<IReadOnlyCollection<string>, ValueTask<I
99117 } ) ) ;
100118
101119 _cSharpParseOptions = new CSharpParseOptions ( LanguageVersion . Preview ) ;
120+ return Task . CompletedTask ;
102121 }
103122
104123 public async Task < CompileToAssemblyResult > CompileToAssemblyAsync (
@@ -107,14 +126,60 @@ public async Task<CompileToAssemblyResult> CompileToAssemblyAsync(
107126 {
108127 ArgumentNullException . ThrowIfNull ( codeFiles ) ;
109128
129+ var codeHash = ComputeCodeHash ( codeFiles ) ;
130+ if ( _cachedResult != null && _lastCodeHash == codeHash )
131+ {
132+ return _cachedResult ;
133+ }
134+
110135 var cSharpResults = await this . CompileToCSharpAsync ( codeFiles , updateStatusFunc ) ;
111136
112137 await ( updateStatusFunc ? . Invoke ( "Compiling Assembly" ) ?? Task . CompletedTask ) ;
113138 var result = CompileToAssembly ( cSharpResults ) ;
114139
140+ _lastCodeHash = codeHash ;
141+ _cachedResult = result ;
142+
115143 return result ;
116144 }
117145
146+ private static int ComputeCodeHash ( ICollection < CodeFile > codeFiles )
147+ {
148+ var hash = new HashCode ( ) ;
149+ foreach ( var file in codeFiles )
150+ {
151+ hash . Add ( file . Path ) ;
152+ hash . Add ( file . Content ) ;
153+ }
154+ return hash . ToHashCode ( ) ;
155+ }
156+
157+ // Compiles to a Roslyn compilation reference without emitting a PE image.
158+ // Used for the declaration (phase 1) pass where we only need component type info.
159+ private static ( CompileToCSharpResult error , MetadataReference metadataRef ) CompileToMetadataReference (
160+ IReadOnlyList < CompileToCSharpResult > cSharpResults )
161+ {
162+ if ( cSharpResults . Any ( r => r . Diagnostics . Any ( d => d . Severity == DiagnosticSeverity . Error ) ) )
163+ {
164+ return ( new CompileToCSharpResult { Diagnostics = cSharpResults . SelectMany ( r => r . Diagnostics ) . ToList ( ) } , null ) ;
165+ }
166+
167+ var syntaxTrees = new SyntaxTree [ cSharpResults . Count ] ;
168+ for ( var i = 0 ; i < cSharpResults . Count ; i ++ )
169+ {
170+ syntaxTrees [ i ] = CSharpSyntaxTree . ParseText ( cSharpResults [ i ] . Code , _cSharpParseOptions , cSharpResults [ i ] . FilePath ) ;
171+ }
172+
173+ var compilation = _baseCompilation . AddSyntaxTrees ( syntaxTrees ) ;
174+ var diagnostics = compilation . GetDiagnostics ( ) . Where ( d => d . Severity > DiagnosticSeverity . Info ) . ToList ( ) ;
175+ if ( diagnostics . Any ( d => d . Severity == DiagnosticSeverity . Error ) )
176+ {
177+ return ( new CompileToCSharpResult { Diagnostics = diagnostics . Select ( CompilationDiagnostic . FromCSharpDiagnostic ) . ToList ( ) } , null ) ;
178+ }
179+
180+ return ( null , compilation . ToMetadataReference ( ) ) ;
181+ }
182+
118183 private static CompileToAssemblyResult CompileToAssembly ( IReadOnlyList < CompileToCSharpResult > cSharpResults )
119184 {
120185 if ( cSharpResults . Any ( r => r . Diagnostics . Any ( d => d . Severity == DiagnosticSeverity . Error ) ) )
@@ -144,7 +209,7 @@ private static CompileToAssemblyResult CompileToAssembly(IReadOnlyList<CompileTo
144209
145210 if ( result . Diagnostics . All ( x => x . Severity != DiagnosticSeverity . Error ) )
146211 {
147- using var peStream = new MemoryStream ( ) ;
212+ using var peStream = new MemoryStream ( capacity : 512 * 1024 ) ;
148213 finalCompilation . Emit ( peStream ) ;
149214
150215 result . AssemblyBytes = peStream . ToArray ( ) ;
@@ -164,7 +229,10 @@ private static VirtualProjectItem CreateRazorProjectItem(string fileName, string
164229 filePath = '/' + filePath ;
165230 }
166231
167- fileContent = fileContent . Replace ( "\r " , string . Empty ) ;
232+ if ( fileContent . Contains ( '\r ' ) )
233+ {
234+ fileContent = fileContent . Replace ( "\r " , string . Empty ) ;
235+ }
168236
169237 return new VirtualProjectItem (
170238 WorkingDirectory ,
@@ -180,7 +248,8 @@ private async Task<IReadOnlyList<CompileToCSharpResult>> CompileToCSharpAsync(
180248 Func < string , Task > updateStatusFunc )
181249 {
182250 // The first phase won't include any metadata references for component discovery. This mirrors what the build does.
183- var projectEngine = this . CreateRazorProjectEngine ( Array . Empty < MetadataReference > ( ) ) ;
251+ _declarationProjectEngine ??= this . CreateRazorProjectEngine ( Array . Empty < MetadataReference > ( ) ) ;
252+ var projectEngine = _declarationProjectEngine ;
184253
185254 // Result of generating declarations
186255 var declarations = new CompileToCSharpResult [ codeFiles . Count ] ;
@@ -217,15 +286,15 @@ private async Task<IReadOnlyList<CompileToCSharpResult>> CompileToCSharpAsync(
217286 index ++ ;
218287 }
219288
220- // Result of doing 'temp' compilation
221- var tempAssembly = CompileToAssembly ( declarations ) ;
222- if ( tempAssembly . Diagnostics . Any ( d => d . Severity == DiagnosticSeverity . Error ) )
289+ // Result of doing 'temp' compilation (no emit needed — only used as a metadata reference)
290+ var ( tempErrors , tempRef ) = CompileToMetadataReference ( declarations ) ;
291+ if ( tempErrors != null )
223292 {
224- return [ new CompileToCSharpResult { Diagnostics = tempAssembly . Diagnostics } ] ;
293+ return [ tempErrors ] ;
225294 }
226295
227296 // Add the 'temp' compilation as a metadata reference
228- var references = new List < MetadataReference > ( _baseCompilation . References ) { tempAssembly . Compilation . ToMetadataReference ( ) } ;
297+ var references = new List < MetadataReference > ( _baseCompilation . References ) { tempRef } ;
229298 projectEngine = CreateRazorProjectEngine ( references ) ;
230299
231300 await ( updateStatusFunc ? . Invoke ( "Preparing Project" ) ?? Task . CompletedTask ) ;
0 commit comments