@@ -23,59 +23,35 @@ public override bool Execute()
2323 {
2424 var assetsById = StaticWebAsset . ToAssetDictionary ( CandidateAssets ) ;
2525
26- var endpointsByAsset = CandidateEndpoints . Select ( StaticWebAssetEndpoint . FromTaskItem )
27- . GroupBy ( e => e . AssetFile )
28- . ToDictionary ( g => g . Key , g => g . ToList ( ) ) ;
26+ var endpointsByAsset = StaticWebAssetEndpoint . ToAssetFileDictionary ( CandidateEndpoints ) ;
2927
30- var compressedAssets = assetsById . Values . Where ( a => a . AssetTraitName == "Content-Encoding" ) . ToList ( ) ;
31- var updatedEndpoints = new HashSet < StaticWebAssetEndpoint > ( StaticWebAssetEndpoint . RouteAndAssetComparer ) ;
28+ var updatedEndpoints = new HashSet < StaticWebAssetEndpoint > ( CandidateEndpoints . Length , StaticWebAssetEndpoint . RouteAndAssetComparer ) ;
3229
33- var preservedEndpoints = new Dictionary < ( string , string ) , StaticWebAssetEndpoint > ( ) ;
30+ var compressionHeadersByEncoding = new Dictionary < string , StaticWebAssetEndpointResponseHeader [ ] > ( 2 ) ;
3431
3532 // Add response headers to compressed endpoints
36- foreach ( var compressedAsset in compressedAssets )
33+ foreach ( var compressedAsset in assetsById . Values )
3734 {
38- if ( ! assetsById . TryGetValue ( compressedAsset . RelatedAsset , out var relatedAsset ) )
35+ if ( ! string . Equals ( compressedAsset . AssetTraitName , "Content-Encoding" , StringComparison . Ordinal ) )
3936 {
40- Log . LogWarning ( "Related asset not found for compressed asset: {0}" , compressedAsset . Identity ) ;
41- throw new InvalidOperationException ( $ "Related asset not found for compressed asset: { compressedAsset . Identity } ") ;
42- }
43-
44- if ( ! endpointsByAsset . TryGetValue ( compressedAsset . Identity , out var compressedEndpoints ) )
45- {
46- Log . LogWarning ( "Endpoints not found for compressed asset: {0} {1}" , compressedAsset . RelativePath , compressedAsset . Identity ) ;
47- throw new InvalidOperationException ( $ "Endpoints not found for compressed asset: { compressedAsset . Identity } ") ;
37+ continue ;
4838 }
4939
50- if ( ! endpointsByAsset . TryGetValue ( relatedAsset . Identity , out var relatedAssetEndpoints ) )
51- {
52- Log . LogWarning ( "Endpoints not found for related asset: {0}" , relatedAsset . Identity ) ;
53- throw new InvalidOperationException ( $ "Endpoints not found for related asset: { relatedAsset . Identity } ") ;
54- }
40+ var ( compressedEndpoints , relatedAssetEndpoints ) = ResolveEndpoints ( assetsById , endpointsByAsset , compressedAsset ) ;
5541
5642 Log . LogMessage ( "Processing compressed asset: {0}" , compressedAsset . Identity ) ;
57- StaticWebAssetEndpointResponseHeader [ ] compressionHeaders = [
58- new ( )
59- {
60- Name = "Content-Encoding" ,
61- Value = compressedAsset . AssetTraitValue
62- } ,
63- new ( )
64- {
65- Name = "Vary" ,
66- Value = "Content-Encoding"
67- }
68- ] ;
43+ var compressionHeaders = GetOrCreateCompressionHeaders ( compressionHeadersByEncoding , compressedAsset ) ;
6944
7045 var quality = ResolveQuality ( compressedAsset ) ;
7146 foreach ( var compressedEndpoint in compressedEndpoints )
7247 {
73- if ( compressedEndpoint . Selectors . Any ( s => string . Equals ( s . Name , "Content-Encoding" , StringComparison . Ordinal ) ) )
48+ if ( HasContentEncodingSelector ( compressedEndpoint ) )
7449 {
75- Log . LogMessage ( MessageImportance . Low , $ " Skipping endpoint '{ compressedEndpoint . Route } ' since it already has a Content-Encoding selector") ;
50+ Log . LogMessage ( MessageImportance . Low , " Skipping endpoint '{0 }' since it already has a Content-Encoding selector" , compressedEndpoint . Route ) ;
7651 continue ;
7752 }
78- if ( ! compressedEndpoint . ResponseHeaders . Any ( s => string . Equals ( s . Name , "Content-Encoding" , StringComparison . Ordinal ) ) )
53+
54+ if ( ! HasContentEncodingResponseHeader ( compressedEndpoint ) )
7955 {
8056 // Add the Content-Encoding and Vary headers
8157 compressedEndpoint . ResponseHeaders = [
@@ -84,6 +60,8 @@ public override bool Execute()
8460 ] ;
8561 }
8662
63+ var compressedHeaders = GetCompressedHeaders ( compressedEndpoint ) ;
64+
8765 Log . LogMessage ( MessageImportance . Low , " Updated endpoint '{0}' with Content-Encoding and Vary headers" , compressedEndpoint . Route ) ;
8866 updatedEndpoints . Add ( compressedEndpoint ) ;
8967
@@ -93,55 +71,18 @@ public override bool Execute()
9371 {
9472 continue ;
9573 }
96- Log . LogMessage ( MessageImportance . Low , "Processing related endpoint '{0}'" , relatedEndpointCandidate . Route ) ;
97- var encodingSelector = new StaticWebAssetEndpointSelector
98- {
99- Name = "Content-Encoding" ,
100- Value = compressedAsset . AssetTraitValue ,
101- Quality = quality
102- } ;
103- Log . LogMessage ( MessageImportance . Low , " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'" , encodingSelector . Value , encodingSelector . Quality , relatedEndpointCandidate . Route ) ;
104- var endpointCopy = new StaticWebAssetEndpoint
105- {
106- AssetFile = compressedAsset . Identity ,
107- Route = relatedEndpointCandidate . Route ,
108- Selectors = [
109- ..relatedEndpointCandidate . Selectors ,
110- encodingSelector
111- ] ,
112- EndpointProperties = [ .. relatedEndpointCandidate . EndpointProperties ]
113- } ;
114-
115- var headers = new List < StaticWebAssetEndpointResponseHeader > ( ) ;
116- var compressedHeaders = new HashSet < string > ( compressedEndpoint . ResponseHeaders . Select ( h => h . Name ) , StringComparer . Ordinal ) ;
117- ApplyCompressedEndpointHeaders ( headers , compressedEndpoint , relatedEndpointCandidate . Route ) ;
118- ApplyRelatedEndpointCandidateHeaders ( headers , relatedEndpointCandidate , compressedHeaders ) ;
119- endpointCopy . ResponseHeaders = [ .. headers ] ;
120-
121- // Update the endpoint
122- Log . LogMessage ( MessageImportance . Low , " Updated related endpoint '{0}' with Content-Encoding selector '{1}={2}'" , relatedEndpointCandidate . Route , encodingSelector . Value , encodingSelector . Quality ) ;
123- updatedEndpoints . Add ( endpointCopy ) ;
12474
75+ var endpointCopy = CreateUpdatedEndpoint ( compressedAsset , quality , compressedEndpoint , compressedHeaders , relatedEndpointCandidate ) ;
76+ updatedEndpoints . Add ( endpointCopy ) ;
12577 // Since we are going to remove the endpoints from the associated item group and the route is
12678 // the ItemSpec, we want to add the original as well so that it gets re-added.
12779 // The endpoint pointing to the uncompressed asset doesn't have a Content-Encoding selector and
12880 // will use the default "identity" encoding during content negotiation.
129- if ( ! preservedEndpoints . ContainsKey ( ( relatedEndpointCandidate . Route , relatedEndpointCandidate . AssetFile ) ) )
130- {
131- preservedEndpoints . Add (
132- ( relatedEndpointCandidate . Route , relatedEndpointCandidate . AssetFile ) ,
133- relatedEndpointCandidate ) ;
134- }
81+ updatedEndpoints . Add ( relatedEndpointCandidate ) ;
13582 }
13683 }
13784 }
13885
139- // Add the preserved endpoints to the list of updated endpoints.
140- foreach ( var preservedEndpoint in preservedEndpoints . Values )
141- {
142- updatedEndpoints . Add ( preservedEndpoint ) ;
143- }
144-
14586 // Before we return the updated endpoints we need to capture any other endpoint whose asset is not associated
14687 // with the compressed asset. This is because we are going to remove the endpoints from the associated item group
14788 // and the route is the ItemSpec, so it will cause those endpoints to be removed.
@@ -169,11 +110,11 @@ public override bool Execute()
169110 // We now have only endpoints that might have the same route but point to different assets
170111 // and we want to include them in the updated endpoints so that we don't incorrectly remove
171112 // them from the associated item group when we update the endpoints.
172- var endpointsByRoute = endpointsByAsset . Values . SelectMany ( e => e ) . GroupBy ( e => e . Route ) . ToDictionary ( g => g . Key , g => g . ToList ( ) ) ;
173-
174- var updatedEndpointsByRoute = updatedEndpoints . Select ( e => e . Route ) . ToArray ( ) ;
175- foreach ( var route in updatedEndpointsByRoute )
113+ var endpointsByRoute = GetEndpointsByRoute ( endpointsByAsset ) ;
114+ var additionalUpdatedEndpoints = new HashSet < StaticWebAssetEndpoint > ( updatedEndpoints . Count , StaticWebAssetEndpoint . RouteAndAssetComparer ) ;
115+ foreach ( var updatedEndpoint in updatedEndpoints )
176116 {
117+ var route = updatedEndpoint . Route ;
177118 Log . LogMessage ( MessageImportance . Low , "Processing route '{0}'" , route ) ;
178119 if ( endpointsByRoute . TryGetValue ( route , out var endpoints ) )
179120 {
@@ -184,26 +125,187 @@ public override bool Execute()
184125 }
185126 foreach ( var endpoint in endpoints )
186127 {
187- updatedEndpoints . Add ( endpoint ) ;
128+ additionalUpdatedEndpoints . Add ( endpoint ) ;
188129 }
189130 }
190131 }
191132
192- UpdatedEndpoints = updatedEndpoints . Distinct ( ) . Select ( e => e . ToTaskItem ( ) ) . ToArray ( ) ;
133+ updatedEndpoints . UnionWith ( additionalUpdatedEndpoints ) ;
134+
135+ UpdatedEndpoints = StaticWebAssetEndpoint . ToTaskItems ( updatedEndpoints ) ;
193136
194137 return true ;
195138 }
196139
140+ private static HashSet < string > GetCompressedHeaders ( StaticWebAssetEndpoint compressedEndpoint )
141+ {
142+ var result = new HashSet < string > ( compressedEndpoint . ResponseHeaders . Length , StringComparer . Ordinal ) ;
143+ for ( var i = 0 ; i < compressedEndpoint . ResponseHeaders . Length ; i ++ )
144+ {
145+ var responseHeader = compressedEndpoint . ResponseHeaders [ i ] ;
146+ result . Add ( responseHeader . Name ) ;
147+ }
148+
149+ return result ;
150+ }
151+
152+ private static Dictionary < string , List < StaticWebAssetEndpoint > > GetEndpointsByRoute (
153+ IDictionary < string , List < StaticWebAssetEndpoint > > endpointsByAsset )
154+ {
155+ var result = new Dictionary < string , List < StaticWebAssetEndpoint > > ( endpointsByAsset . Count ) ;
156+
157+ foreach ( var endpointsList in endpointsByAsset . Values )
158+ {
159+ foreach ( var endpoint in endpointsList )
160+ {
161+ if ( ! result . TryGetValue ( endpoint . Route , out var routeEndpoints ) )
162+ {
163+ routeEndpoints = new List < StaticWebAssetEndpoint > ( 5 ) ;
164+ result [ endpoint . Route ] = routeEndpoints ;
165+ }
166+ routeEndpoints . Add ( endpoint ) ;
167+ }
168+ }
169+
170+ return result ;
171+ }
172+
173+ private static StaticWebAssetEndpointResponseHeader [ ] GetOrCreateCompressionHeaders ( Dictionary < string , StaticWebAssetEndpointResponseHeader [ ] > compressionHeadersByEncoding , StaticWebAsset compressedAsset )
174+ {
175+ if ( ! compressionHeadersByEncoding . TryGetValue ( compressedAsset . AssetTraitValue , out var compressionHeaders ) )
176+ {
177+ compressionHeaders = CreateCompressionHeaders ( compressedAsset ) ;
178+ compressionHeadersByEncoding . Add ( compressedAsset . AssetTraitValue , compressionHeaders ) ;
179+ }
180+
181+ return compressionHeaders ;
182+ }
183+
184+ private static StaticWebAssetEndpointResponseHeader [ ] CreateCompressionHeaders ( StaticWebAsset compressedAsset ) =>
185+ [
186+ new ( )
187+ {
188+ Name = "Content-Encoding" ,
189+ Value = compressedAsset . AssetTraitValue
190+ } ,
191+ new ( )
192+ {
193+ Name = "Vary" ,
194+ Value = "Content-Encoding"
195+ }
196+ ] ;
197+
198+ private StaticWebAssetEndpoint CreateUpdatedEndpoint (
199+ StaticWebAsset compressedAsset ,
200+ string quality ,
201+ StaticWebAssetEndpoint compressedEndpoint ,
202+ HashSet < string > compressedHeaders ,
203+ StaticWebAssetEndpoint relatedEndpointCandidate )
204+ {
205+ Log . LogMessage ( MessageImportance . Low , "Processing related endpoint '{0}'" , relatedEndpointCandidate . Route ) ;
206+ var encodingSelector = new StaticWebAssetEndpointSelector
207+ {
208+ Name = "Content-Encoding" ,
209+ Value = compressedAsset . AssetTraitValue ,
210+ Quality = quality
211+ } ;
212+ Log . LogMessage ( MessageImportance . Low , " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'" , encodingSelector . Value , encodingSelector . Quality , relatedEndpointCandidate . Route ) ;
213+ var endpointCopy = new StaticWebAssetEndpoint
214+ {
215+ AssetFile = compressedAsset . Identity ,
216+ Route = relatedEndpointCandidate . Route ,
217+ Selectors = [
218+ ..relatedEndpointCandidate . Selectors ,
219+ encodingSelector
220+ ] ,
221+ EndpointProperties = relatedEndpointCandidate . EndpointProperties
222+ } ;
223+ var headers = new List < StaticWebAssetEndpointResponseHeader > ( 7 ) ;
224+ ApplyCompressedEndpointHeaders ( headers , compressedEndpoint , relatedEndpointCandidate . Route ) ;
225+ ApplyRelatedEndpointCandidateHeaders ( headers , relatedEndpointCandidate , compressedHeaders ) ;
226+ endpointCopy . ResponseHeaders = [ .. headers ] ;
227+
228+ // Update the endpoint
229+ Log . LogMessage ( MessageImportance . Low , " Updated related endpoint '{0}' with Content-Encoding selector '{1}={2}'" , relatedEndpointCandidate . Route , encodingSelector . Value , encodingSelector . Quality ) ;
230+ return endpointCopy ;
231+ }
232+
233+ private static bool HasContentEncodingResponseHeader ( StaticWebAssetEndpoint compressedEndpoint )
234+ {
235+ for ( var i = 0 ; i < compressedEndpoint . ResponseHeaders . Length ; i ++ )
236+ {
237+ var responseHeader = compressedEndpoint . ResponseHeaders [ i ] ;
238+ if ( string . Equals ( responseHeader . Name , "Content-Encoding" , StringComparison . Ordinal ) )
239+ {
240+ return true ;
241+ }
242+ }
243+
244+ return false ;
245+ }
246+
247+ private static bool HasContentEncodingSelector ( StaticWebAssetEndpoint compressedEndpoint )
248+ {
249+ for ( var i = 0 ; i < compressedEndpoint . Selectors . Length ; i ++ )
250+ {
251+ var selector = compressedEndpoint . Selectors [ i ] ;
252+ if ( string . Equals ( selector . Name , "Content-Encoding" , StringComparison . Ordinal ) )
253+ {
254+ return true ;
255+ }
256+ }
257+
258+ return false ;
259+ }
260+
261+ private ( List < StaticWebAssetEndpoint > compressedEndpoints , List < StaticWebAssetEndpoint > relatedAssetEndpoints ) ResolveEndpoints (
262+ IDictionary < string , StaticWebAsset > assetsById ,
263+ IDictionary < string , List < StaticWebAssetEndpoint > > endpointsByAsset ,
264+ StaticWebAsset compressedAsset )
265+ {
266+ if ( ! assetsById . TryGetValue ( compressedAsset . RelatedAsset , out var relatedAsset ) )
267+ {
268+ Log . LogWarning ( "Related asset not found for compressed asset: {0}" , compressedAsset . Identity ) ;
269+ throw new InvalidOperationException ( $ "Related asset not found for compressed asset: { compressedAsset . Identity } ") ;
270+ }
271+
272+ if ( ! endpointsByAsset . TryGetValue ( compressedAsset . Identity , out var compressedEndpoints ) )
273+ {
274+ Log . LogWarning ( "Endpoints not found for compressed asset: {0} {1}" , compressedAsset . RelativePath , compressedAsset . Identity ) ;
275+ throw new InvalidOperationException ( $ "Endpoints not found for compressed asset: { compressedAsset . Identity } ") ;
276+ }
277+
278+ if ( ! endpointsByAsset . TryGetValue ( relatedAsset . Identity , out var relatedAssetEndpoints ) )
279+ {
280+ Log . LogWarning ( "Endpoints not found for related asset: {0}" , relatedAsset . Identity ) ;
281+ throw new InvalidOperationException ( $ "Endpoints not found for related asset: { relatedAsset . Identity } ") ;
282+ }
283+
284+ return ( compressedEndpoints , relatedAssetEndpoints ) ;
285+ }
286+
197287 private static string ResolveQuality ( StaticWebAsset compressedAsset ) =>
198288 Math . Round ( 1.0 / ( compressedAsset . FileLength + 1 ) , 12 ) . ToString ( "F12" , CultureInfo . InvariantCulture ) ;
199289
200290 private static bool IsCompatible ( StaticWebAssetEndpoint compressedEndpoint , StaticWebAssetEndpoint relatedEndpointCandidate )
201291 {
202- var compressedFingerprint = compressedEndpoint . EndpointProperties . FirstOrDefault ( ep => ep . Name == "fingerprint" ) ;
203- var relatedFingerprint = relatedEndpointCandidate . EndpointProperties . FirstOrDefault ( ep => ep . Name == "fingerprint" ) ;
292+ var compressedFingerprint = ResolveFingerprint ( compressedEndpoint ) ;
293+ var relatedFingerprint = ResolveFingerprint ( relatedEndpointCandidate ) ;
204294 return string . Equals ( compressedFingerprint . Value , relatedFingerprint . Value , StringComparison . Ordinal ) ;
205295 }
206296
297+ private static StaticWebAssetEndpointProperty ResolveFingerprint ( StaticWebAssetEndpoint compressedEndpoint )
298+ {
299+ foreach ( var property in compressedEndpoint . EndpointProperties )
300+ {
301+ if ( string . Equals ( property . Name , "fingerprint" , StringComparison . Ordinal ) )
302+ {
303+ return property ;
304+ }
305+ }
306+ return default ;
307+ }
308+
207309 private void ApplyCompressedEndpointHeaders ( List < StaticWebAssetEndpointResponseHeader > headers , StaticWebAssetEndpoint compressedEndpoint , string relatedEndpointCandidateRoute )
208310 {
209311 foreach ( var header in compressedEndpoint . ResponseHeaders )
0 commit comments