44using System ;
55using System . Collections . Immutable ;
66using System . ComponentModel . Composition ;
7+ using System . Diagnostics ;
8+ using System . Linq ;
9+ using System . Runtime . InteropServices ;
710using System . Threading ;
811using System . Threading . Tasks ;
912using Microsoft . AspNetCore . Razor . Language ;
@@ -87,39 +90,52 @@ protected virtual async ValueTask<ImmutableArray<TagHelperDescriptor>> ResolveTa
8790
8891 if ( deltaResult is null )
8992 {
93+ // For some reason, TryInvokeAsync can return null if it is cancelled while fetching the client.
9094 return default ;
9195 }
9296
9397 // Apply the delta we received to any cached checksums for the current project.
9498 var checksums = ProduceChecksumsFromDelta ( project . Id , lastResultId , deltaResult ) ;
9599
96- using var tagHelpers = new PooledArrayBuilder < TagHelperDescriptor > ( capacity : checksums . Length ) ;
97- using var checksumsToFetch = new PooledArrayBuilder < Checksum > ( capacity : checksums . Length ) ;
100+ // Create an array to hold the result. We'll wrap it in an ImmutableArray at the end.
101+ var result = new TagHelperDescriptor [ checksums . Length ] ;
98102
99- foreach ( var checksum in checksums )
103+ // We need to keep track of which checksums we still need to fetch tag helpers for from OOP.
104+ // In addition, we'll track the indices in tagHelpers that we'll need to replace with those we
105+ // fetch to ensure that the results stay in the same order.
106+ using var checksumsToFetchBuilder = new PooledArrayBuilder < Checksum > ( capacity : checksums . Length ) ;
107+ using var checksumIndicesBuilder = new PooledArrayBuilder < int > ( capacity : checksums . Length ) ;
108+
109+ for ( var i = 0 ; i < checksums . Length ; i ++ )
100110 {
111+ var checksum = checksums [ i ] ;
112+
101113 // See if we have a cached version of this tag helper. If not, we'll need to fetch it from OOP.
102114 if ( TagHelperCache . Default . TryGet ( checksum , out var tagHelper ) )
103115 {
104- tagHelpers . Add ( tagHelper ) ;
116+ result [ i ] = tagHelper ;
105117 }
106118 else
107119 {
108- checksumsToFetch . Add ( checksum ) ;
120+ checksumsToFetchBuilder . Add ( checksum ) ;
121+ checksumIndicesBuilder . Add ( i ) ;
109122 }
110123 }
111124
112- if ( checksumsToFetch . Count > 0 )
125+ if ( checksumsToFetchBuilder . Count > 0 )
113126 {
127+ var checksumsToFetch = checksumsToFetchBuilder . DrainToImmutable ( ) ;
128+
114129 // There are checksums that we don't have cached tag helpers for, so we need to fetch them from OOP.
115130 var fetchResult = await _remoteServiceInvoker . TryInvokeAsync < IRemoteTagHelperProviderService , FetchTagHelpersResult > (
116131 project . Solution ,
117132 ( service , solutionInfo , innerCancellationToken ) =>
118- service . FetchTagHelpersAsync ( solutionInfo , projectHandle , checksumsToFetch . DrainToImmutable ( ) , innerCancellationToken ) ,
133+ service . FetchTagHelpersAsync ( solutionInfo , projectHandle , checksumsToFetch , innerCancellationToken ) ,
119134 cancellationToken ) ;
120135
121136 if ( fetchResult is null )
122137 {
138+ // For some reason, TryInvokeAsync can return null if it is cancelled while fetching the client.
123139 return default ;
124140 }
125141
@@ -131,16 +147,46 @@ protected virtual async ValueTask<ImmutableArray<TagHelperDescriptor>> ResolveTa
131147 throw new InvalidOperationException ( "Tag helpers could not be fetched from the Roslyn OOP." ) ;
132148 }
133149
150+ Debug . Assert (
151+ checksumsToFetch . Length == fetchedTagHelpers . Length ,
152+ $ "{ nameof ( FetchTagHelpersResult ) } should return the same number of tag helpers as checksums requested.") ;
153+
154+ Debug . Assert (
155+ checksumsToFetch . SequenceEqual ( fetchedTagHelpers . Select ( static t => t . Checksum ) ) ,
156+ $ "{ nameof ( FetchTagHelpersResult ) } should return tag helpers that match the checksums requested.") ;
157+
134158 // Be sure to add the tag helpers we just fetched to the cache.
135159 var cache = TagHelperCache . Default ;
136- foreach ( var tagHelper in fetchedTagHelpers )
160+
161+ for ( var i = 0 ; i < fetchedTagHelpers . Length ; i ++ )
162+ {
163+ var index = checksumIndicesBuilder [ i ] ;
164+ Debug . Assert ( result [ index ] is null ) ;
165+
166+ var fetchedTagHelper = fetchedTagHelpers [ i ] ;
167+ result [ index ] = fetchedTagHelper ;
168+ cache . TryAdd ( fetchedTagHelper . Checksum , fetchedTagHelper ) ;
169+ }
170+
171+ if ( checksumsToFetch . Length != fetchedTagHelpers . Length )
137172 {
138- tagHelpers . Add ( tagHelper ) ;
139- cache . TryAdd ( tagHelper . Checksum , tagHelper ) ;
173+ // We didn't receive all the tag helpers we requested. This is bad. However, instead of failing,
174+ // we'll just return the tag helpers we were able to retrieve.
175+ using var resultBuilder = new PooledArrayBuilder < TagHelperDescriptor > ( capacity : result . Length ) ;
176+
177+ foreach ( var tagHelper in result )
178+ {
179+ if ( tagHelper is not null )
180+ {
181+ resultBuilder . Add ( tagHelper ) ;
182+ }
183+ }
184+
185+ return resultBuilder . DrainToImmutable ( ) ;
140186 }
141187 }
142188
143- return tagHelpers . DrainToImmutable ( ) ;
189+ return ImmutableCollectionsMarshal . AsImmutableArray ( result ) ;
144190 }
145191
146192 // Protected virtual for testing
0 commit comments