@@ -27,66 +27,31 @@ public override bool Execute()
2727
2828 var updatedEndpoints = new HashSet < StaticWebAssetEndpoint > ( CandidateEndpoints . Length , StaticWebAssetEndpoint . RouteAndAssetComparer ) ;
2929
30- var preservedEndpoints = new Dictionary < ( string , string ) , StaticWebAssetEndpoint > ( CandidateEndpoints . Length ) ;
31-
32- var compressionHeadersByContentEncoding = new Dictionary < string , StaticWebAssetEndpointResponseHeader [ ] > ( 2 ) ;
30+ var compressionHeadersByEncoding = new Dictionary < string , StaticWebAssetEndpointResponseHeader [ ] > ( 2 ) ;
3331
3432 // Add response headers to compressed endpoints
35- foreach ( var kvp in assetsById )
33+ foreach ( var compressedAsset in assetsById . Values )
3634 {
37- var key = kvp . Key ;
38- var compressedAsset = kvp . Value ;
39-
4035 if ( ! string . Equals ( compressedAsset . AssetTraitName , "Content-Encoding" , StringComparison . Ordinal ) )
4136 {
4237 continue ;
4338 }
4439
45- if ( ! assetsById . TryGetValue ( compressedAsset . RelatedAsset , out var relatedAsset ) )
46- {
47- Log . LogWarning ( "Related asset not found for compressed asset: {0}" , compressedAsset . Identity ) ;
48- throw new InvalidOperationException ( $ "Related asset not found for compressed asset: { compressedAsset . Identity } ") ;
49- }
50-
51- if ( ! endpointsByAsset . TryGetValue ( compressedAsset . Identity , out var compressedEndpoints ) )
52- {
53- Log . LogWarning ( "Endpoints not found for compressed asset: {0} {1}" , compressedAsset . RelativePath , compressedAsset . Identity ) ;
54- throw new InvalidOperationException ( $ "Endpoints not found for compressed asset: { compressedAsset . Identity } ") ;
55- }
56-
57- if ( ! endpointsByAsset . TryGetValue ( relatedAsset . Identity , out var relatedAssetEndpoints ) )
58- {
59- Log . LogWarning ( "Endpoints not found for related asset: {0}" , relatedAsset . Identity ) ;
60- throw new InvalidOperationException ( $ "Endpoints not found for related asset: { relatedAsset . Identity } ") ;
61- }
40+ var ( compressedEndpoints , relatedAssetEndpoints ) = ResolveEndpoints ( assetsById , endpointsByAsset , compressedAsset ) ;
6241
6342 Log . LogMessage ( "Processing compressed asset: {0}" , compressedAsset . Identity ) ;
64- if ( compressionHeadersByContentEncoding . TryGetValue ( compressedAsset . AssetTraitValue , out var compressionHeaders ) )
65- {
66- compressionHeaders = [
67- new ( )
68- {
69- Name = "Content-Encoding" ,
70- Value = compressedAsset . AssetTraitValue
71- } ,
72- new ( )
73- {
74- Name = "Vary" ,
75- Value = "Content-Encoding"
76- }
77- ] ;
78- compressionHeadersByContentEncoding . Add ( compressedAsset . AssetTraitValue , compressionHeaders ) ;
79- }
43+ var compressionHeaders = GetOrCreateCompressionHeaders ( compressionHeadersByEncoding , compressedAsset ) ;
8044
8145 var quality = ResolveQuality ( compressedAsset ) ;
8246 foreach ( var compressedEndpoint in compressedEndpoints )
8347 {
84- if ( compressedEndpoint . Selectors . Any ( s => string . Equals ( s . Name , "Content-Encoding" , StringComparison . Ordinal ) ) )
48+ if ( HasContentEncodingSelector ( compressedEndpoint ) )
8549 {
86- 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 ) ;
8751 continue ;
8852 }
89- if ( ! compressedEndpoint . ResponseHeaders . Any ( s => string . Equals ( s . Name , "Content-Encoding" , StringComparison . Ordinal ) ) )
53+
54+ if ( ! HasContentEncodingResponseHeader ( compressedEndpoint ) )
9055 {
9156 // Add the Content-Encoding and Vary headers
9257 compressedEndpoint . ResponseHeaders = [
@@ -95,66 +60,29 @@ public override bool Execute()
9560 ] ;
9661 }
9762
63+ var compressedHeaders = GetCompressedHeaders ( compressedEndpoint ) ;
64+
9865 Log . LogMessage ( MessageImportance . Low , " Updated endpoint '{0}' with Content-Encoding and Vary headers" , compressedEndpoint . Route ) ;
9966 updatedEndpoints . Add ( compressedEndpoint ) ;
10067
101- var compressedHeaders = GetCompressedHeaders ( compressedEndpoint ) ;
10268 foreach ( var relatedEndpointCandidate in relatedAssetEndpoints )
10369 {
10470 if ( ! IsCompatible ( compressedEndpoint , relatedEndpointCandidate ) )
10571 {
10672 continue ;
10773 }
108- Log . LogMessage ( MessageImportance . Low , "Processing related endpoint '{0}'" , relatedEndpointCandidate . Route ) ;
109- var encodingSelector = new StaticWebAssetEndpointSelector
110- {
111- Name = "Content-Encoding" ,
112- Value = compressedAsset . AssetTraitValue ,
113- Quality = quality
114- } ;
115- Log . LogMessage ( MessageImportance . Low , " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'" , encodingSelector . Value , encodingSelector . Quality , relatedEndpointCandidate . Route ) ;
116- var endpointCopy = new StaticWebAssetEndpoint
117- {
118- AssetFile = compressedAsset . Identity ,
119- Route = relatedEndpointCandidate . Route ,
120- Selectors = [
121- ..relatedEndpointCandidate . Selectors ,
122- encodingSelector
123- ] ,
124- // Endpoint properties are never modified as part of this step, so we can just copy them and avoid extra work
125- EndpointProperties = relatedEndpointCandidate . EndpointProperties
126- } ;
127-
128- var headers = new List < StaticWebAssetEndpointResponseHeader > ( 7 ) ;
129-
130- ApplyCompressedEndpointHeaders ( headers , compressedEndpoint , relatedEndpointCandidate . Route ) ;
131- ApplyRelatedEndpointCandidateHeaders ( headers , relatedEndpointCandidate , compressedHeaders ) ;
132- endpointCopy . ResponseHeaders = [ .. headers ] ;
13374
134- // Update the endpoint
135- Log . LogMessage ( MessageImportance . Low , " Updated related endpoint '{0}' with Content-Encoding selector '{1}={2}'" , relatedEndpointCandidate . Route , encodingSelector . Value , encodingSelector . Quality ) ;
75+ var endpointCopy = CreateUpdatedEndpoint ( compressedAsset , quality , compressedEndpoint , compressedHeaders , relatedEndpointCandidate ) ;
13676 updatedEndpoints . Add ( endpointCopy ) ;
137-
13877 // Since we are going to remove the endpoints from the associated item group and the route is
13978 // the ItemSpec, we want to add the original as well so that it gets re-added.
14079 // The endpoint pointing to the uncompressed asset doesn't have a Content-Encoding selector and
14180 // will use the default "identity" encoding during content negotiation.
142- if ( ! preservedEndpoints . ContainsKey ( ( relatedEndpointCandidate . Route , relatedEndpointCandidate . AssetFile ) ) )
143- {
144- preservedEndpoints . Add (
145- ( relatedEndpointCandidate . Route , relatedEndpointCandidate . AssetFile ) ,
146- relatedEndpointCandidate ) ;
147- }
81+ updatedEndpoints . Add ( relatedEndpointCandidate ) ;
14882 }
14983 }
15084 }
15185
152- // Add the preserved endpoints to the list of updated endpoints.
153- foreach ( var preservedEndpoint in preservedEndpoints . Values )
154- {
155- updatedEndpoints . Add ( preservedEndpoint ) ;
156- }
157-
15886 // Before we return the updated endpoints we need to capture any other endpoint whose asset is not associated
15987 // with the compressed asset. This is because we are going to remove the endpoints from the associated item group
16088 // and the route is the ItemSpec, so it will cause those endpoints to be removed.
@@ -182,11 +110,11 @@ public override bool Execute()
182110 // We now have only endpoints that might have the same route but point to different assets
183111 // and we want to include them in the updated endpoints so that we don't incorrectly remove
184112 // them from the associated item group when we update the endpoints.
185- var endpointsByRoute = endpointsByAsset . Values . SelectMany ( e => e ) . GroupBy ( e => e . Route ) . ToDictionary ( g => g . Key , g => g . ToList ( ) ) ;
186-
187- var updatedEndpointsByRoute = updatedEndpoints . Select ( e => e . Route ) . ToArray ( ) ;
188- 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 )
189116 {
117+ var route = updatedEndpoint . Route ;
190118 Log . LogMessage ( MessageImportance . Low , "Processing route '{0}'" , route ) ;
191119 if ( endpointsByRoute . TryGetValue ( route , out var endpoints ) )
192120 {
@@ -197,36 +125,187 @@ public override bool Execute()
197125 }
198126 foreach ( var endpoint in endpoints )
199127 {
200- updatedEndpoints . Add ( endpoint ) ;
128+ additionalUpdatedEndpoints . Add ( endpoint ) ;
201129 }
202130 }
203131 }
204132
205- UpdatedEndpoints = updatedEndpoints . Distinct ( ) . Select ( e => e . ToTaskItem ( ) ) . ToArray ( ) ;
133+ updatedEndpoints . UnionWith ( additionalUpdatedEndpoints ) ;
134+
135+ UpdatedEndpoints = StaticWebAssetEndpoint . ToTaskItems ( updatedEndpoints ) ;
206136
207137 return true ;
208138 }
209139
210140 private static HashSet < string > GetCompressedHeaders ( StaticWebAssetEndpoint compressedEndpoint )
211141 {
212- var result = new HashSet < string > ( compressedEndpoint . Selectors . Length , StringComparer . Ordinal ) ;
142+ var result = new HashSet < string > ( compressedEndpoint . ResponseHeaders . Length , StringComparer . Ordinal ) ;
213143 for ( var i = 0 ; i < compressedEndpoint . ResponseHeaders . Length ; i ++ )
214144 {
215- result . Add ( compressedEndpoint . ResponseHeaders [ i ] . Name ) ;
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+ }
216168 }
169+
217170 return result ;
218171 }
219172
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+
220287 private static string ResolveQuality ( StaticWebAsset compressedAsset ) =>
221288 Math . Round ( 1.0 / ( compressedAsset . FileLength + 1 ) , 12 ) . ToString ( "F12" , CultureInfo . InvariantCulture ) ;
222289
223290 private static bool IsCompatible ( StaticWebAssetEndpoint compressedEndpoint , StaticWebAssetEndpoint relatedEndpointCandidate )
224291 {
225- var compressedFingerprint = compressedEndpoint . EndpointProperties . FirstOrDefault ( ep => ep . Name == "fingerprint" ) ;
226- var relatedFingerprint = relatedEndpointCandidate . EndpointProperties . FirstOrDefault ( ep => ep . Name == "fingerprint" ) ;
292+ var compressedFingerprint = ResolveFingerprint ( compressedEndpoint ) ;
293+ var relatedFingerprint = ResolveFingerprint ( relatedEndpointCandidate ) ;
227294 return string . Equals ( compressedFingerprint . Value , relatedFingerprint . Value , StringComparison . Ordinal ) ;
228295 }
229296
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+
230309 private void ApplyCompressedEndpointHeaders ( List < StaticWebAssetEndpointResponseHeader > headers , StaticWebAssetEndpoint compressedEndpoint , string relatedEndpointCandidateRoute )
231310 {
232311 foreach ( var header in compressedEndpoint . ResponseHeaders )
0 commit comments