@@ -23,59 +23,35 @@ public override bool Execute()
23
23
{
24
24
var assetsById = StaticWebAsset . ToAssetDictionary ( CandidateAssets ) ;
25
25
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 ) ;
29
27
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 ) ;
32
29
33
- var preservedEndpoints = new Dictionary < ( string , string ) , StaticWebAssetEndpoint > ( ) ;
30
+ var compressionHeadersByEncoding = new Dictionary < string , StaticWebAssetEndpointResponseHeader [ ] > ( 2 ) ;
34
31
35
32
// Add response headers to compressed endpoints
36
- foreach ( var compressedAsset in compressedAssets )
33
+ foreach ( var compressedAsset in assetsById . Values )
37
34
{
38
- if ( ! assetsById . TryGetValue ( compressedAsset . RelatedAsset , out var relatedAsset ) )
35
+ if ( ! string . Equals ( compressedAsset . AssetTraitName , "Content-Encoding" , StringComparison . Ordinal ) )
39
36
{
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 ;
48
38
}
49
39
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 ) ;
55
41
56
42
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 ) ;
69
44
70
45
var quality = ResolveQuality ( compressedAsset ) ;
71
46
foreach ( var compressedEndpoint in compressedEndpoints )
72
47
{
73
- if ( compressedEndpoint . Selectors . Any ( s => string . Equals ( s . Name , "Content-Encoding" , StringComparison . Ordinal ) ) )
48
+ if ( HasContentEncodingSelector ( compressedEndpoint ) )
74
49
{
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 ) ;
76
51
continue ;
77
52
}
78
- if ( ! compressedEndpoint . ResponseHeaders . Any ( s => string . Equals ( s . Name , "Content-Encoding" , StringComparison . Ordinal ) ) )
53
+
54
+ if ( ! HasContentEncodingResponseHeader ( compressedEndpoint ) )
79
55
{
80
56
// Add the Content-Encoding and Vary headers
81
57
compressedEndpoint . ResponseHeaders = [
@@ -84,6 +60,8 @@ public override bool Execute()
84
60
] ;
85
61
}
86
62
63
+ var compressedHeaders = GetCompressedHeaders ( compressedEndpoint ) ;
64
+
87
65
Log . LogMessage ( MessageImportance . Low , " Updated endpoint '{0}' with Content-Encoding and Vary headers" , compressedEndpoint . Route ) ;
88
66
updatedEndpoints . Add ( compressedEndpoint ) ;
89
67
@@ -93,55 +71,18 @@ public override bool Execute()
93
71
{
94
72
continue ;
95
73
}
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 ) ;
124
74
75
+ var endpointCopy = CreateUpdatedEndpoint ( compressedAsset , quality , compressedEndpoint , compressedHeaders , relatedEndpointCandidate ) ;
76
+ updatedEndpoints . Add ( endpointCopy ) ;
125
77
// Since we are going to remove the endpoints from the associated item group and the route is
126
78
// the ItemSpec, we want to add the original as well so that it gets re-added.
127
79
// The endpoint pointing to the uncompressed asset doesn't have a Content-Encoding selector and
128
80
// 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 ) ;
135
82
}
136
83
}
137
84
}
138
85
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
-
145
86
// Before we return the updated endpoints we need to capture any other endpoint whose asset is not associated
146
87
// with the compressed asset. This is because we are going to remove the endpoints from the associated item group
147
88
// and the route is the ItemSpec, so it will cause those endpoints to be removed.
@@ -169,11 +110,11 @@ public override bool Execute()
169
110
// We now have only endpoints that might have the same route but point to different assets
170
111
// and we want to include them in the updated endpoints so that we don't incorrectly remove
171
112
// 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 )
176
116
{
117
+ var route = updatedEndpoint . Route ;
177
118
Log . LogMessage ( MessageImportance . Low , "Processing route '{0}'" , route ) ;
178
119
if ( endpointsByRoute . TryGetValue ( route , out var endpoints ) )
179
120
{
@@ -184,26 +125,187 @@ public override bool Execute()
184
125
}
185
126
foreach ( var endpoint in endpoints )
186
127
{
187
- updatedEndpoints . Add ( endpoint ) ;
128
+ additionalUpdatedEndpoints . Add ( endpoint ) ;
188
129
}
189
130
}
190
131
}
191
132
192
- UpdatedEndpoints = updatedEndpoints . Distinct ( ) . Select ( e => e . ToTaskItem ( ) ) . ToArray ( ) ;
133
+ updatedEndpoints . UnionWith ( additionalUpdatedEndpoints ) ;
134
+
135
+ UpdatedEndpoints = StaticWebAssetEndpoint . ToTaskItems ( updatedEndpoints ) ;
193
136
194
137
return true ;
195
138
}
196
139
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
+
197
287
private static string ResolveQuality ( StaticWebAsset compressedAsset ) =>
198
288
Math . Round ( 1.0 / ( compressedAsset . FileLength + 1 ) , 12 ) . ToString ( "F12" , CultureInfo . InvariantCulture ) ;
199
289
200
290
private static bool IsCompatible ( StaticWebAssetEndpoint compressedEndpoint , StaticWebAssetEndpoint relatedEndpointCandidate )
201
291
{
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 ) ;
204
294
return string . Equals ( compressedFingerprint . Value , relatedFingerprint . Value , StringComparison . Ordinal ) ;
205
295
}
206
296
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
+
207
309
private void ApplyCompressedEndpointHeaders ( List < StaticWebAssetEndpointResponseHeader > headers , StaticWebAssetEndpoint compressedEndpoint , string relatedEndpointCandidateRoute )
208
310
{
209
311
foreach ( var header in compressedEndpoint . ResponseHeaders )
0 commit comments