Reduce StaticFileContext allocation/struct copy (#9815)

This commit is contained in:
Ben Adams 2019-05-03 19:55:37 +01:00 committed by Chris Ross
parent 3c40df452d
commit 617bc1ac02
3 changed files with 224 additions and 171 deletions

View File

@ -2,13 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Endpoints;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Headers;
@ -22,129 +22,99 @@ namespace Microsoft.AspNetCore.StaticFiles
internal struct StaticFileContext internal struct StaticFileContext
{ {
private const int StreamCopyBufferSize = 64 * 1024; private const int StreamCopyBufferSize = 64 * 1024;
private readonly HttpContext _context; private readonly HttpContext _context;
private readonly StaticFileOptions _options; private readonly StaticFileOptions _options;
private readonly PathString _matchUrl;
private readonly HttpRequest _request; private readonly HttpRequest _request;
private readonly HttpResponse _response; private readonly HttpResponse _response;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IFileProvider _fileProvider; private readonly IFileProvider _fileProvider;
private readonly IContentTypeProvider _contentTypeProvider; private readonly string _method;
private string _method; private readonly string _contentType;
private bool _isGet;
private bool _isHead;
private PathString _subPath;
private string _contentType;
private IFileInfo _fileInfo;
private long _length;
private DateTimeOffset _lastModified;
private EntityTagHeaderValue _etag;
private IFileInfo _fileInfo;
private EntityTagHeaderValue _etag;
private RequestHeaders _requestHeaders; private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders; private ResponseHeaders _responseHeaders;
private RangeItemHeaderValue _range;
private long _length;
private readonly PathString _subPath;
private DateTimeOffset _lastModified;
private PreconditionState _ifMatchState; private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState; private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState; private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState; private PreconditionState _ifUnmodifiedSinceState;
private RangeItemHeaderValue _range; private RequestType _requestType;
private bool _isRangeRequest;
public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl, ILogger logger, IFileProvider fileProvider, IContentTypeProvider contentTypeProvider) public StaticFileContext(HttpContext context, StaticFileOptions options, ILogger logger, IFileProvider fileProvider, string contentType, PathString subPath)
{ {
_context = context; _context = context;
_options = options; _options = options;
_matchUrl = matchUrl;
_request = context.Request; _request = context.Request;
_response = context.Response; _response = context.Response;
_logger = logger; _logger = logger;
_requestHeaders = _request.GetTypedHeaders();
_responseHeaders = _response.GetTypedHeaders();
_fileProvider = fileProvider; _fileProvider = fileProvider;
_contentTypeProvider = contentTypeProvider; _method = _request.Method;
_contentType = contentType;
_method = null;
_isGet = false;
_isHead = false;
_subPath = PathString.Empty;
_contentType = null;
_fileInfo = null; _fileInfo = null;
_length = 0;
_lastModified = new DateTimeOffset();
_etag = null; _etag = null;
_requestHeaders = null;
_responseHeaders = null;
_range = null;
_length = 0;
_subPath = subPath;
_lastModified = new DateTimeOffset();
_ifMatchState = PreconditionState.Unspecified; _ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified; _ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified; _ifModifiedSinceState = PreconditionState.Unspecified;
_ifUnmodifiedSinceState = PreconditionState.Unspecified; _ifUnmodifiedSinceState = PreconditionState.Unspecified;
_range = null;
_isRangeRequest = false; if (HttpMethods.IsGet(_method))
{
_requestType = RequestType.IsGet;
}
else if (HttpMethods.IsHead(_method))
{
_requestType = RequestType.IsHead;
}
else
{
_requestType = RequestType.Unspecified;
}
} }
internal enum PreconditionState private RequestHeaders RequestHeaders => (_requestHeaders ??= _request.GetTypedHeaders());
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}
public bool IsHeadMethod private ResponseHeaders ResponseHeaders => (_responseHeaders ??= _response.GetTypedHeaders());
{
get { return _isHead; } public bool IsHeadMethod => _requestType.HasFlag(RequestType.IsHead);
}
public bool IsGetMethod => _requestType.HasFlag(RequestType.IsGet);
public bool IsRangeRequest public bool IsRangeRequest
{ {
get { return _isRangeRequest; } get => _requestType.HasFlag(RequestType.IsRange);
} private set
public string SubPath
{
get { return _subPath.Value; }
}
public string PhysicalPath
{
get { return _fileInfo?.PhysicalPath; }
}
public bool ValidateNoEndpoint()
{
// Return true because we only want to run if there is no endpoint.
return _context.GetEndpoint() == null;
}
public bool ValidateMethod()
{
_method = _request.Method;
_isGet = HttpMethods.IsGet(_method);
_isHead = HttpMethods.IsHead(_method);
return _isGet || _isHead;
}
// Check if the URL matches any expected paths
public bool ValidatePath()
{
return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath);
}
public bool LookupContentType()
{
if (_contentTypeProvider.TryGetContentType(_subPath.Value, out _contentType))
{ {
return true; if (value)
{
_requestType |= RequestType.IsRange;
}
else
{
_requestType &= ~RequestType.IsRange;
}
} }
if (_options.ServeUnknownFileTypes)
{
_contentType = _options.DefaultContentType;
return true;
}
return false;
} }
public string SubPath => _subPath.Value;
public string PhysicalPath => _fileInfo?.PhysicalPath;
public bool LookupFileInfo() public bool LookupFileInfo()
{ {
_fileInfo = _fileProvider.GetFileInfo(_subPath.Value); _fileInfo = _fileProvider.GetFileInfo(_subPath.Value);
@ -175,8 +145,10 @@ namespace Microsoft.AspNetCore.StaticFiles
private void ComputeIfMatch() private void ComputeIfMatch()
{ {
var requestHeaders = RequestHeaders;
// 14.24 If-Match // 14.24 If-Match
var ifMatch = _requestHeaders.IfMatch; var ifMatch = requestHeaders.IfMatch;
if (ifMatch != null && ifMatch.Any()) if (ifMatch != null && ifMatch.Any())
{ {
_ifMatchState = PreconditionState.PreconditionFailed; _ifMatchState = PreconditionState.PreconditionFailed;
@ -191,7 +163,7 @@ namespace Microsoft.AspNetCore.StaticFiles
} }
// 14.26 If-None-Match // 14.26 If-None-Match
var ifNoneMatch = _requestHeaders.IfNoneMatch; var ifNoneMatch = requestHeaders.IfNoneMatch;
if (ifNoneMatch != null && ifNoneMatch.Any()) if (ifNoneMatch != null && ifNoneMatch.Any())
{ {
_ifNoneMatchState = PreconditionState.ShouldProcess; _ifNoneMatchState = PreconditionState.ShouldProcess;
@ -208,10 +180,11 @@ namespace Microsoft.AspNetCore.StaticFiles
private void ComputeIfModifiedSince() private void ComputeIfModifiedSince()
{ {
var requestHeaders = RequestHeaders;
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
// 14.25 If-Modified-Since // 14.25 If-Modified-Since
var ifModifiedSince = _requestHeaders.IfModifiedSince; var ifModifiedSince = requestHeaders.IfModifiedSince;
if (ifModifiedSince.HasValue && ifModifiedSince <= now) if (ifModifiedSince.HasValue && ifModifiedSince <= now)
{ {
bool modified = ifModifiedSince < _lastModified; bool modified = ifModifiedSince < _lastModified;
@ -219,7 +192,7 @@ namespace Microsoft.AspNetCore.StaticFiles
} }
// 14.28 If-Unmodified-Since // 14.28 If-Unmodified-Since
var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince; var ifUnmodifiedSince = requestHeaders.IfUnmodifiedSince;
if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now) if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
{ {
bool unmodified = ifUnmodifiedSince >= _lastModified; bool unmodified = ifUnmodifiedSince >= _lastModified;
@ -230,7 +203,7 @@ namespace Microsoft.AspNetCore.StaticFiles
private void ComputeIfRange() private void ComputeIfRange()
{ {
// 14.27 If-Range // 14.27 If-Range
var ifRangeHeader = _requestHeaders.IfRange; var ifRangeHeader = RequestHeaders.IfRange;
if (ifRangeHeader != null) if (ifRangeHeader != null)
{ {
// If the validator given in the If-Range header field matches the // If the validator given in the If-Range header field matches the
@ -242,12 +215,12 @@ namespace Microsoft.AspNetCore.StaticFiles
{ {
if (_lastModified > ifRangeHeader.LastModified) if (_lastModified > ifRangeHeader.LastModified)
{ {
_isRangeRequest = false; IsRangeRequest = false;
} }
} }
else if (_etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true)) else if (_etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true))
{ {
_isRangeRequest = false; IsRangeRequest = false;
} }
} }
} }
@ -259,12 +232,15 @@ namespace Microsoft.AspNetCore.StaticFiles
// A server MUST ignore a Range header field received with a request method other // A server MUST ignore a Range header field received with a request method other
// than GET. // than GET.
if (!_isGet) if (!IsGetMethod)
{ {
return; return;
} }
(_isRangeRequest, _range) = RangeHelper.ParseRange(_context, _requestHeaders, _length, _logger); (var isRangeRequest, var range) = RangeHelper.ParseRange(_context, RequestHeaders, _length, _logger);
_range = range;
IsRangeRequest = isRangeRequest;
} }
public void ApplyResponseHeaders(int statusCode) public void ApplyResponseHeaders(int statusCode)
@ -278,9 +254,11 @@ namespace Microsoft.AspNetCore.StaticFiles
{ {
_response.ContentType = _contentType; _response.ContentType = _contentType;
} }
_responseHeaders.LastModified = _lastModified;
_responseHeaders.ETag = _etag; var responseHeaders = ResponseHeaders;
_responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes"; responseHeaders.LastModified = _lastModified;
responseHeaders.ETag = _etag;
responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
} }
if (statusCode == Constants.Status200Ok) if (statusCode == Constants.Status200Ok)
{ {
@ -294,10 +272,7 @@ namespace Microsoft.AspNetCore.StaticFiles
} }
public PreconditionState GetPreconditionState() public PreconditionState GetPreconditionState()
{ => GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, _ifModifiedSinceState, _ifUnmodifiedSinceState);
return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState,
_ifModifiedSinceState, _ifUnmodifiedSinceState);
}
private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states) private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
{ {
@ -320,6 +295,52 @@ namespace Microsoft.AspNetCore.StaticFiles
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task ServeStaticFile(HttpContext context, RequestDelegate next)
{
ComprehendRequestHeaders();
switch (GetPreconditionState())
{
case PreconditionState.Unspecified:
case PreconditionState.ShouldProcess:
if (IsHeadMethod)
{
await SendStatusAsync(Constants.Status200Ok);
return;
}
try
{
if (IsRangeRequest)
{
await SendRangeAsync();
return;
}
await SendAsync();
_logger.FileServed(SubPath, PhysicalPath);
return;
}
catch (FileNotFoundException)
{
context.Response.Clear();
}
await next(context);
return;
case PreconditionState.NotModified:
_logger.FileNotModified(SubPath);
await SendStatusAsync(Constants.Status304NotModified);
return;
case PreconditionState.PreconditionFailed:
_logger.PreconditionFailed(SubPath);
await SendStatusAsync(Constants.Status412PreconditionFailed);
return;
default:
var exception = new NotImplementedException(GetPreconditionState().ToString());
Debug.Fail(exception.ToString());
throw exception;
}
}
public async Task SendAsync() public async Task SendAsync()
{ {
SetCompressionMode(); SetCompressionMode();
@ -358,14 +379,14 @@ namespace Microsoft.AspNetCore.StaticFiles
// 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable) // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
// SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies // SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
// the current length of the selected resource. e.g. */length // the current length of the selected resource. e.g. */length
_responseHeaders.ContentRange = new ContentRangeHeaderValue(_length); ResponseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable); ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);
_logger.RangeNotSatisfiable(SubPath); _logger.RangeNotSatisfiable(SubPath);
return; return;
} }
_responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length); ResponseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
_response.ContentLength = length; _response.ContentLength = length;
SetCompressionMode(); SetCompressionMode();
ApplyResponseHeaders(Constants.Status206PartialContent); ApplyResponseHeaders(Constants.Status206PartialContent);
@ -416,5 +437,22 @@ namespace Microsoft.AspNetCore.StaticFiles
responseCompressionFeature.Mode = _options.HttpsCompression; responseCompressionFeature.Mode = _options.HttpsCompression;
} }
} }
internal enum PreconditionState : byte
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}
[Flags]
private enum RequestType : byte
{
Unspecified = 0b_000,
IsHead = 0b_001,
IsGet = 0b_010,
IsRange = 0b_100,
}
} }
} }

View File

@ -2,12 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Endpoints;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -70,83 +69,83 @@ namespace Microsoft.AspNetCore.StaticFiles
/// <returns></returns> /// <returns></returns>
public Task Invoke(HttpContext context) public Task Invoke(HttpContext context)
{ {
var fileContext = new StaticFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider); if (!ValidateNoEndpoint(context))
if (!fileContext.ValidateNoEndpoint())
{ {
_logger.EndpointMatched(); _logger.EndpointMatched();
} }
else if (!fileContext.ValidateMethod()) else if (!ValidateMethod(context))
{ {
_logger.RequestMethodNotSupported(context.Request.Method); _logger.RequestMethodNotSupported(context.Request.Method);
} }
else if (!fileContext.ValidatePath()) else if (!ValidatePath(context, _matchUrl, out var subPath))
{ {
_logger.PathMismatch(fileContext.SubPath); _logger.PathMismatch(subPath);
} }
else if (!fileContext.LookupContentType()) else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
{ {
_logger.FileTypeNotSupported(fileContext.SubPath); _logger.FileTypeNotSupported(subPath);
} }
else if (!fileContext.LookupFileInfo()) else
{
// If we get here, we can try to serve the file
return TryServeStaticFile(context, contentType, subPath);
}
return _next(context);
}
// Return true because we only want to run if there is no endpoint.
private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;
private static bool ValidateMethod(HttpContext context)
{
var method = context.Request.Method;
var isValid = false;
if (HttpMethods.IsGet(method))
{
isValid = true;
}
else if (HttpMethods.IsHead(method))
{
isValid = true;
}
return isValid;
}
internal static bool ValidatePath(HttpContext context, PathString matchUrl, out PathString subPath) => Helpers.TryMatchPath(context, matchUrl, forDirectory: false, out subPath);
internal static bool LookupContentType(IContentTypeProvider contentTypeProvider, StaticFileOptions options, PathString subPath, out string contentType)
{
if (contentTypeProvider.TryGetContentType(subPath.Value, out contentType))
{
return true;
}
if (options.ServeUnknownFileTypes)
{
contentType = options.DefaultContentType;
return true;
}
return false;
}
private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath)
{
var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);
if (!fileContext.LookupFileInfo())
{ {
_logger.FileNotFound(fileContext.SubPath); _logger.FileNotFound(fileContext.SubPath);
} }
else else
{ {
// If we get here, we can try to serve the file // If we get here, we can try to serve the file
return ServeStaticFile(context, fileContext); return fileContext.ServeStaticFile(context, _next);
} }
return _next(context); return _next(context);
} }
private async Task ServeStaticFile(HttpContext context, StaticFileContext fileContext)
{
fileContext.ComprehendRequestHeaders();
switch (fileContext.GetPreconditionState())
{
case StaticFileContext.PreconditionState.Unspecified:
case StaticFileContext.PreconditionState.ShouldProcess:
if (fileContext.IsHeadMethod)
{
await fileContext.SendStatusAsync(Constants.Status200Ok);
return;
}
try
{
if (fileContext.IsRangeRequest)
{
await fileContext.SendRangeAsync();
return;
}
await fileContext.SendAsync();
_logger.FileServed(fileContext.SubPath, fileContext.PhysicalPath);
return;
}
catch (FileNotFoundException)
{
context.Response.Clear();
}
await _next(context);
return;
case StaticFileContext.PreconditionState.NotModified:
_logger.FileNotModified(fileContext.SubPath);
await fileContext.SendStatusAsync(Constants.Status304NotModified);
return;
case StaticFileContext.PreconditionState.PreconditionFailed:
_logger.PreconditionFailed(fileContext.SubPath);
await fileContext.SendStatusAsync(Constants.Status412PreconditionFailed);
return;
default:
var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
Debug.Fail(exception.ToString());
throw exception;
}
}
} }
} }

View File

@ -22,14 +22,18 @@ namespace Microsoft.AspNetCore.StaticFiles
{ {
// Arrange // Arrange
var options = new StaticFileOptions(); var options = new StaticFileOptions();
var context = new StaticFileContext(new DefaultHttpContext(), options, PathString.Empty, NullLogger.Instance, new TestFileProvider(), new FileExtensionContentTypeProvider()); var httpContext = new DefaultHttpContext();
var pathString = PathString.Empty;
var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
var context = new StaticFileContext(httpContext, options, NullLogger.Instance, new TestFileProvider(), contentType, subPath);
// Act // Act
var validateResult = context.ValidatePath();
var lookupResult = context.LookupFileInfo(); var lookupResult = context.LookupFileInfo();
// Assert // Assert
Assert.True(validateResult); Assert.True(validateResult);
Assert.False(contentTypeResult);
Assert.False(lookupResult); Assert.False(lookupResult);
} }
@ -46,13 +50,17 @@ namespace Microsoft.AspNetCore.StaticFiles
var pathString = new PathString("/test"); var pathString = new PathString("/test");
var httpContext = new DefaultHttpContext(); var httpContext = new DefaultHttpContext();
httpContext.Request.Path = new PathString("/test/foo.txt"); httpContext.Request.Path = new PathString("/test/foo.txt");
var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider()); var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
var context = new StaticFileContext(httpContext, options, NullLogger.Instance, fileProvider, contentType, subPath);
// Act // Act
context.ValidatePath();
var result = context.LookupFileInfo(); var result = context.LookupFileInfo();
// Assert // Assert
Assert.True(validateResult);
Assert.True(contentTypeResult);
Assert.True(result); Assert.True(result);
} }
@ -70,10 +78,14 @@ namespace Microsoft.AspNetCore.StaticFiles
var httpsCompressionFeature = new TestHttpsCompressionFeature(); var httpsCompressionFeature = new TestHttpsCompressionFeature();
httpContext.Features.Set<IHttpsCompressionFeature>(httpsCompressionFeature); httpContext.Features.Set<IHttpsCompressionFeature>(httpsCompressionFeature);
httpContext.Request.Path = new PathString("/test/foo.txt"); httpContext.Request.Path = new PathString("/test/foo.txt");
var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider()); var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
var context = new StaticFileContext(httpContext, options, NullLogger.Instance, fileProvider, contentType, subPath);
context.ValidatePath();
var result = context.LookupFileInfo(); var result = context.LookupFileInfo();
Assert.True(validateResult);
Assert.True(contentTypeResult);
Assert.True(result); Assert.True(result);
await context.SendAsync(); await context.SendAsync();
@ -95,10 +107,14 @@ namespace Microsoft.AspNetCore.StaticFiles
var httpsCompressionFeature = new TestHttpsCompressionFeature(); var httpsCompressionFeature = new TestHttpsCompressionFeature();
httpContext.Features.Set<IHttpsCompressionFeature>(httpsCompressionFeature); httpContext.Features.Set<IHttpsCompressionFeature>(httpsCompressionFeature);
httpContext.Request.Path = new PathString("/test/bar.txt"); httpContext.Request.Path = new PathString("/test/bar.txt");
var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider()); var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
var context = new StaticFileContext(httpContext, options, NullLogger.Instance, fileProvider, contentType, subPath);
context.ValidatePath();
var result = context.LookupFileInfo(); var result = context.LookupFileInfo();
Assert.True(validateResult);
Assert.True(contentTypeResult);
Assert.False(result); Assert.False(result);
Assert.Equal(HttpsCompressionMode.Default, httpsCompressionFeature.Mode); Assert.Equal(HttpsCompressionMode.Default, httpsCompressionFeature.Mode);