10
10
using Microsoft . AspNetCore . Http ;
11
11
using Microsoft . AspNetCore . Http . Features ;
12
12
using Microsoft . AspNetCore . Routing ;
13
+ using Microsoft . AspNetCore . Routing . Patterns ;
13
14
using Microsoft . AspNetCore . StaticAssets ;
14
15
using Microsoft . Extensions . Configuration ;
15
16
using Microsoft . Extensions . DependencyInjection ;
16
17
using Microsoft . Extensions . FileProviders ;
17
18
using Microsoft . Extensions . Hosting ;
19
+ using Microsoft . Extensions . Logging ;
18
20
using Microsoft . Extensions . Primitives ;
19
21
using Microsoft . Net . Http . Headers ;
20
22
21
23
namespace Microsoft . AspNetCore . Builder ;
22
24
23
25
// Handles changes during development to support common scenarios where for example, a developer changes a file in the wwwroot folder.
24
- internal class StaticAssetDevelopmentRuntimeHandler ( List < StaticAssetDescriptor > descriptors )
26
+ internal sealed partial class StaticAssetDevelopmentRuntimeHandler ( List < StaticAssetDescriptor > descriptors )
25
27
{
26
28
public void AttachRuntimePatching ( EndpointBuilder builder )
27
29
{
@@ -46,7 +48,7 @@ public void AttachRuntimePatching(EndpointBuilder builder)
46
48
47
49
// In case we were dealing with a compressed asset, we are going to wrap the response body feature to re-compress the asset on the fly.
48
50
// and write that to the response instead.
49
- context . Features . Set < IHttpResponseBodyFeature > ( new HotReloadStaticAsset ( originalFeature , context , asset ) ) ;
51
+ context . Features . Set < IHttpResponseBodyFeature > ( new RuntimeStaticAssetResponseBodyFeature ( originalFeature , context , asset ) ) ;
50
52
}
51
53
52
54
await original ( context ) ;
@@ -60,13 +62,13 @@ internal static string GetETag(IFileInfo fileInfo)
60
62
return $ "\" { Convert . ToBase64String ( SHA256 . HashData ( stream ) ) } \" ";
61
63
}
62
64
63
- internal class HotReloadStaticAsset : IHttpResponseBodyFeature
65
+ internal sealed class RuntimeStaticAssetResponseBodyFeature : IHttpResponseBodyFeature
64
66
{
65
67
private readonly IHttpResponseBodyFeature _original ;
66
68
private readonly HttpContext _context ;
67
69
private readonly StaticAssetDescriptor _asset ;
68
70
69
- public HotReloadStaticAsset ( IHttpResponseBodyFeature original , HttpContext context , StaticAssetDescriptor asset )
71
+ public RuntimeStaticAssetResponseBodyFeature ( IHttpResponseBodyFeature original , HttpContext context , StaticAssetDescriptor asset )
70
72
{
71
73
_original = original ;
72
74
_context = context ;
@@ -172,12 +174,16 @@ internal static void EnableSupport(
172
174
173
175
if ( ! disableFallback )
174
176
{
177
+ var logger = endpoints . ServiceProvider . GetRequiredService < ILogger < StaticAssetDevelopmentRuntimeHandler > > ( ) ;
178
+
175
179
// Add a fallback static file handler to serve any file that might have been added after the initial startup.
176
- endpoints . MapFallback (
180
+ var fallback = endpoints . MapFallback (
177
181
"{**path:file}" ,
178
182
endpoints . CreateApplicationBuilder ( )
179
183
. Use ( ( ctx , nxt ) =>
180
184
{
185
+ Log . StaticAssetNotFoundInManifest ( logger , ctx . Request . Path ) ;
186
+
181
187
ctx . SetEndpoint ( null ) ;
182
188
ctx . Response . OnStarting ( ( context ) =>
183
189
{
@@ -197,83 +203,49 @@ internal static void EnableSupport(
197
203
} )
198
204
. UseStaticFiles ( )
199
205
. Build ( ) ) ;
200
- }
201
-
202
- }
203
- }
204
206
205
- internal static class StaticAssetDescriptorExtensions
206
- {
207
- internal static long GetContentLength ( this StaticAssetDescriptor descriptor )
208
- {
209
- foreach ( var header in descriptor . ResponseHeaders )
210
- {
211
- if ( header . Name == "Content-Length" )
212
- {
213
- return long . Parse ( header . Value , CultureInfo . InvariantCulture ) ;
214
- }
207
+ // Set up a custom constraint to only match existing files.
208
+ fallback
209
+ . Add ( endpoint =>
210
+ {
211
+ if ( endpoint is not RouteEndpointBuilder routeEndpoint || routeEndpoint is not { RoutePattern . RawText : { } pattern } )
212
+ {
213
+ return ;
214
+ }
215
+
216
+ // Add a custom constraint (not inline) to check if the file exists as part of the route matching
217
+ routeEndpoint . RoutePattern = RoutePatternFactory . Parse (
218
+ pattern ,
219
+ null ,
220
+ new RouteValueDictionary { [ "path" ] = new FileExistsConstraint ( environment ) } ) ;
221
+ } ) ;
222
+
223
+ // Limit matching to supported methods.
224
+ fallback . Add ( b => b . Metadata . Add ( new HttpMethodMetadata ( [ "GET" , "HEAD" ] ) ) ) ;
215
225
}
216
-
217
- throw new InvalidOperationException ( "Content-Length header not found." ) ;
218
226
}
219
227
220
- internal static DateTimeOffset GetLastModified ( this StaticAssetDescriptor descriptor )
228
+ private static partial class Log
221
229
{
222
- foreach ( var header in descriptor . ResponseHeaders )
223
- {
224
- if ( header . Name == "Last-Modified" )
225
- {
226
- return DateTimeOffset . Parse ( header . Value , CultureInfo . InvariantCulture ) ;
227
- }
228
- }
230
+ private const string StaticAssetNotFoundInManifestMessage = """The static asset '{Path}' was not found in the built time manifest. This file will not be available at runtime if it is not available at compile time during the publish process. If the file was not added to the project during development, and is created at runtime, use the StaticFiles middleware to serve it instead.""" ;
229
231
230
- throw new InvalidOperationException ( "Last-Modified header not found." ) ;
232
+ [ LoggerMessage ( 1 , LogLevel . Warning , StaticAssetNotFoundInManifestMessage ) ]
233
+ public static partial void StaticAssetNotFoundInManifest ( ILogger logger , string path ) ;
231
234
}
232
235
233
- internal static EntityTagHeaderValue GetWeakETag ( this StaticAssetDescriptor descriptor )
236
+ private sealed class FileExistsConstraint ( IWebHostEnvironment environment ) : IRouteConstraint
234
237
{
235
- foreach ( var header in descriptor . ResponseHeaders )
236
- {
237
- if ( header . Name == "ETag" )
238
- {
239
- var eTag = EntityTagHeaderValue . Parse ( header . Value ) ;
240
- if ( eTag . IsWeak )
241
- {
242
- return eTag ;
243
- }
244
- }
245
- }
238
+ private readonly IWebHostEnvironment _environment = environment ;
246
239
247
- throw new InvalidOperationException ( "ETag header not found." ) ;
248
- }
249
-
250
- internal static bool HasContentEncoding ( this StaticAssetDescriptor descriptor )
251
- {
252
- foreach ( var selector in descriptor . Selectors )
240
+ public bool Match ( HttpContext ? httpContext , IRouter ? route , string routeKey , RouteValueDictionary values , RouteDirection routeDirection )
253
241
{
254
- if ( selector . Name == "Content-Encoding" )
242
+ if ( values [ routeKey ] is not string path )
255
243
{
256
- return true ;
244
+ return false ;
257
245
}
258
- }
259
246
260
- return false ;
261
- }
262
-
263
- internal static bool HasETag ( this StaticAssetDescriptor descriptor , string tag )
264
- {
265
- foreach ( var header in descriptor . ResponseHeaders )
266
- {
267
- if ( header . Name == "ETag" )
268
- {
269
- var eTag = EntityTagHeaderValue . Parse ( header . Value ) ;
270
- if ( ! eTag . IsWeak && eTag . Tag == tag )
271
- {
272
- return true ;
273
- }
274
- }
247
+ var fileInfo = _environment . WebRootFileProvider . GetFileInfo ( path ) ;
248
+ return fileInfo . Exists ;
275
249
}
276
-
277
- return false ;
278
250
}
279
251
}
0 commit comments