浅谈ASP.NET Core静态文件处理源码探究

前言

静态文件(如 HTML、CSS、图像和 JavaScript)等是Web程序的重要组成部分。传统的ASP.NET项目一般都是部署在IIS上,IIS是一个功能非常强大的服务器平台,可以直接处理接收到的静态文件处理而不需要经过应用程序池处理,所以很多情况下对于静态文件的处理程序本身是无感知的。ASP.NET Core则不同,作为Server的Kestrel服务是宿主到程序上的,由宿主运行程序启动Server然后可以监听请求,所以通过程序我们直接可以处理静态文件相关。静态文件默认存储到项目的wwwroot目录中,当然我们也可以自定义任意目录去处理静态文件。总之,在ASP.NET Core我们可以处理静态文件相关的请求。

StaticFile三剑客

通常我们在说道静态文件相关的时候会涉及到三个话题分别是启用静态文件、默认静态页面、静态文件目录浏览,在ASP.NET Core分别是通过UseStaticFiles、UseDefaultFiles、UseDirectoryBrowser三个中间件去处理。只有配置了相关中间件才能去操作对应的处理,相信大家对这种操作已经很熟了。静态文件操作相关的源码都位于GitHub aspnetcore仓库中的https://github.com/dotnet/aspnetcore/tree/v3.1.6/src/Middleware/StaticFiles/src目录。接下来我们分别探究这三个中间件的相关代码,来揭开静态文件处理的神秘面纱。

UseStaticFiles

UseStaticFiles中间件使我们处理静态文件时最常使用的中间件,因为只有开启了这个中间件我们才能使用静态文件,比如在使用MVC开发的时候需要私用js css html等文件都需要用到它,使用的方式也比较简单

//使用默认路径,即wwwroot
app.UseStaticFiles();
//或自定义读取路径
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles");
app.UseStaticFiles(new StaticFileOptions {
  RequestPath="/staticfiles",
  FileProvider = fileProvider
});

我们直接找到中间件的注册类StaticFileExtensions[点击查看StaticFileExtensions源码]

public static class StaticFileExtensions
{
  public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
  {
    return app.UseMiddleware<StaticFileMiddleware>();
  }

  public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath)
  {
    return app.UseStaticFiles(new StaticFileOptions
    {
      RequestPath = new PathString(requestPath)
    });
  }

  public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options)
  {
    return app.UseMiddleware<StaticFileMiddleware>(Options.Create(options));
  }
}

一般我们最常用到的是无参的方式和传递自定义StaticFileOptions的方式比较多,StaticFileOptions是自定义使用静态文件时的配置信息类,接下来我们大致看一下具体包含哪些配置项[点击查看StaticFileOptions源码]

public class StaticFileOptions : SharedOptionsBase
{
  public StaticFileOptions() : this(new SharedOptions())
  {
  }

  public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions)
  {
    OnPrepareResponse = _ => { };
  }

  /// <summary>
  /// 文件类型提供程序,也就是我们常用的文件名对应MimeType的对应关系
  /// </summary>
  public IContentTypeProvider ContentTypeProvider { get; set; }

  /// <summary>
  /// 设置该路径下默认文件输出类型
  /// </summary>
  public string DefaultContentType { get; set; }

  public bool ServeUnknownFileTypes { get; set; }

  /// <summary>
  /// 文件压缩方式
  /// </summary>
  public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;

  /// <summary>
  /// 准备输出之前可以做一些自定义操作
  /// </summary>
  public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}

public abstract class SharedOptionsBase
{
  protected SharedOptionsBase(SharedOptions sharedOptions)
  {
    SharedOptions = sharedOptions;
  }

  protected SharedOptions SharedOptions { get; private set; }

  /// <summary>
  /// 请求路径
  /// </summary>
  public PathString RequestPath
  {
    get { return SharedOptions.RequestPath; }
    set { SharedOptions.RequestPath = value; }
  }

  /// <summary>
  /// 文件提供程序,在.NET Core中如果需要访问文件相关操作可使用FileProvider文件提供程序获取文件相关信息
  /// </summary>
  public IFileProvider FileProvider
  {
    get { return SharedOptions.FileProvider; }
    set { SharedOptions.FileProvider = value; }
  }
}

我们自定义静态文件访问时,最常用到的就是RequestPath和FileProvider,一个设置请求路径信息,一个设置读取文件信息。如果需要自定义MimeType映射关系可通过ContentTypeProvider自定义设置映射关系

var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
app.UseStaticFiles(new StaticFileOptions
{
  ContentTypeProvider = provider,
  //可以在输出之前设置输出相关
  OnPrepareResponse = ctx =>
  {
    ctx.Context.Response.Headers.Append("Cache-Control", $"public, max-age=3600");
  }
});

接下来我们步入正题直接查看StaticFileMiddleware中间件的代码[点击查看StaticFileMiddleware源码]

public class StaticFileMiddleware
{
  private readonly StaticFileOptions _options;
  private readonly PathString _matchUrl;
  private readonly RequestDelegate _next;
  private readonly ILogger _logger;
  private readonly IFileProvider _fileProvider;
  private readonly IContentTypeProvider _contentTypeProvider;

  public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
  {
    _next = next;
    _options = options.Value;
    //设置文件类型提供程序
    _contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
    //文件提供程序
    _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
    //匹配路径
    _matchUrl = _options.RequestPath;
    _logger = loggerFactory.CreateLogger<StaticFileMiddleware>();
  }

  public Task Invoke(HttpContext context)
  {
    //判断是够获取到终结点信息,这也就是为什么我们使用UseStaticFiles要在UseRouting之前
    if (!ValidateNoEndpoint(context))
    {
    }
    //判断HttpMethod,只能是Get和Head操作
    else if (!ValidateMethod(context))
    {
    }
    //判断请求路径是否存在
    else if (!ValidatePath(context, _matchUrl, out var subPath))
    {
    }
    //根据请求文件名称判断是否可以匹配到对应的MimeType,如果匹配到则返回contentType
    else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
    {
    }
    else
    {
      //执行静态文件操作
      return TryServeStaticFile(context, contentType, subPath);
    }
    return _next(context);
  }

  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);
    }
    else
    {
      //静态文件处理
      return fileContext.ServeStaticFile(context, _next);
    }
    return _next(context);
  }
}

关于FileExtensionContentTypeProvider这里就不作讲解了,主要是承载文件扩展名和MimeType的映射关系代码不复杂,但是映射关系比较多,有兴趣的可以自行查看FileExtensionContentTypeProvider源码,通过上面我们可以看到,最终执行文件相关操作的是StaticFileContext类[点击查看StaticFileContext源码]

internal struct StaticFileContext
{
  private const int StreamCopyBufferSize = 64 * 1024;

  private readonly HttpContext _context;
  private readonly StaticFileOptions _options;
  private readonly HttpRequest _request;
  private readonly HttpResponse _response;
  private readonly ILogger _logger;
  private readonly IFileProvider _fileProvider;
  private readonly string _method;
  private readonly string _contentType;

  private IFileInfo _fileInfo;
  private EntityTagHeaderValue _etag;
  private RequestHeaders _requestHeaders;
  private ResponseHeaders _responseHeaders;
  private RangeItemHeaderValue _range;

  private long _length;
  private readonly PathString _subPath;
  private DateTimeOffset _lastModified;

  private PreconditionState _ifMatchState;
  private PreconditionState _ifNoneMatchState;
  private PreconditionState _ifModifiedSinceState;
  private PreconditionState _ifUnmodifiedSinceState;

  private RequestType _requestType;

  public StaticFileContext(HttpContext context, StaticFileOptions options, ILogger logger, IFileProvider fileProvider, string contentType, PathString subPath)
  {
    _context = context;
    _options = options;
    _request = context.Request;
    _response = context.Response;
    _logger = logger;
    _fileProvider = fileProvider;
    _method = _request.Method;
    _contentType = contentType;
    _fileInfo = null;
    _etag = null;
    _requestHeaders = null;
    _responseHeaders = null;
    _range = null;

    _length = 0;
    _subPath = subPath;
    _lastModified = new DateTimeOffset();
    _ifMatchState = PreconditionState.Unspecified;
    _ifNoneMatchState = PreconditionState.Unspecified;
    _ifModifiedSinceState = PreconditionState.Unspecified;
    _ifUnmodifiedSinceState = PreconditionState.Unspecified;
    //再次判断请求HttpMethod
    if (HttpMethods.IsGet(_method))
    {
      _requestType = RequestType.IsGet;
    }
    else if (HttpMethods.IsHead(_method))
    {
      _requestType = RequestType.IsHead;
    }
    else
    {
      _requestType = RequestType.Unspecified;
    }
  }

  /// <summary>
  /// 判断文件是否存在
  /// </summary>
  public bool LookupFileInfo()
  {
    //判断根据请求路径是否可以获取到文件信息
    _fileInfo = _fileProvider.GetFileInfo(_subPath.Value);
    if (_fileInfo.Exists)
    {
      //获取文件长度
      _length = _fileInfo.Length;
      //最后修改日期
      DateTimeOffset last = _fileInfo.LastModified;
      _lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
      //ETag标识
      long etagHash = _lastModified.ToFileTime() ^ _length;
      _etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    }
    return _fileInfo.Exists;
  }

  /// <summary>
  /// 处理文件输出
  /// </summary>
  public async Task ServeStaticFile(HttpContext context, RequestDelegate next)
  {
    //1.准备输出相关Header,主要是获取和输出静态文件输出缓存相关的内容
    //2.我们之前提到的OnPrepareResponse也是在这里执行的
    ComprehendRequestHeaders();
    //根据ComprehendRequestHeaders方法获取到的文件状态进行判断
    switch (GetPreconditionState())
    {
      case PreconditionState.Unspecified:
      //处理文件输出
      case PreconditionState.ShouldProcess:
        //判断是否是Head请求
        if (IsHeadMethod)
        {
          await SendStatusAsync(Constants.Status200Ok);
          return;
        }
        try
        {
          //判断是否包含range请求,即文件分段下载的情况
          if (IsRangeRequest)
          {
            await SendRangeAsync();
            return;
          }
          //正常文件输出处理
          await SendAsync();
          _logger.FileServed(SubPath, PhysicalPath);
          return;
        }
        catch (FileNotFoundException)
        {
          context.Response.Clear();
        }
        await next(context);
        return;
      case PreconditionState.NotModified:
        await SendStatusAsync(Constants.Status304NotModified);
        return;
      case PreconditionState.PreconditionFailed:
        await SendStatusAsync(Constants.Status412PreconditionFailed);
        return;
      default:
        var exception = new NotImplementedException(GetPreconditionState().ToString());
        throw exception;
    }
  }

  /// <summary>
  /// 通用文件文件返回处理
  /// </summary>
  public async Task SendAsync()
  {
    SetCompressionMode();
    ApplyResponseHeaders(Constants.Status200Ok);
    string physicalPath = _fileInfo.PhysicalPath;
    var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
    //判断是否设置过输出特征操作相关,比如是否启动输出压缩,或者自定义的输出处理比如输出加密等等
    if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
    {
      await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
      return;
    }
    try
    {
      //不存在任何特殊处理的操作作,直接读取文件返回
      using (var readStream = _fileInfo.CreateReadStream())
      {
        await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
      }
    }
    catch (OperationCanceledException ex)
    {
      _context.Abort();
    }
  }

  /// <summary>
  /// 分段请求下载操作处理
  /// </summary>
  internal async Task SendRangeAsync()
  {
    if (_range == null)
    {
      ResponseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
      ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);
      _logger.RangeNotSatisfiable(SubPath);
      return;
    }
    //计算range相关header数据
    ResponseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
    _response.ContentLength = length;
    //设置输出压缩相关header
    SetCompressionMode();
    ApplyResponseHeaders(Constants.Status206PartialContent);

    string physicalPath = _fileInfo.PhysicalPath;
    var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
    //判断是否设置过输出特征操作相关,比如是否启动输出压缩,或者自定义的输出处理比如输出加密等等
    if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
    {
      _logger.SendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath);
      await sendFile.SendFileAsync(physicalPath, start, length, CancellationToken.None);
      return;
    }
    try
    {
      using (var readStream = _fileInfo.CreateReadStream())
      {
        readStream.Seek(start, SeekOrigin.Begin);
        _logger.CopyingFileRange(_response.Headers[HeaderNames.ContentRange], SubPath);
        //设置文件输出起始位置和读取长度
        await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted);
      }
    }
    catch (OperationCanceledException ex)
    {
      _context.Abort();
    }
  }
}

关的读取设置和处理,其此次是针对正常返回和分段返回的情况,在返回之前判断是否有对输出做特殊处理的情况,比如输出压缩或者自定义的其他输出操作的IHttpResponseBodyFeature,分段返回和正常返回相比主要是多了一部分关于Http头Content-Range相关的设置,对于读取本身其实只是读取的起始位置和读取长度的差别。

UseDirectoryBrowser

目录浏览允许在指定目录中列出目录里的文件及子目录。出于安全方面考虑默认情况下是关闭的可以通过UseDirectoryBrowser中间件开启指定目录浏览功能。通常情况下我们会这样使用

//启用默认目录浏览,即wwwroot
app.UseDirectoryBrowser();
//或自定义指定目录浏览
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/MyImages");
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
  RequestPath = "/MyImages",
  FileProvider = fileProvider
});

开启之后当我们访问https://

/MyImages地址的时候将会展示如下效果,通过一个表格展示目录里的文件信息等

到中间件注册类[点击查看DirectoryBrowserExtensions源码]

public static class DirectoryBrowserExtensions
{
  public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
  {
    return app.UseMiddleware<DirectoryBrowserMiddleware>();
  }

  public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath)
  {
    return app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
      RequestPath = new PathString(requestPath)
    });
  }

  public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options)
  {
    return app.UseMiddleware<DirectoryBrowserMiddleware>(Options.Create(options));
  }
}

这个中间件启用的重载方法和UseStaticFiles类似最终都是在传递DirectoryBrowserOptions,接下来我们就看DirectoryBrowserOptions传递了哪些信息[点击查看DirectoryBrowserOptions源码]

public class DirectoryBrowserOptions : SharedOptionsBase
{
  public DirectoryBrowserOptions()
    : this(new SharedOptions())
  {
  }

  public DirectoryBrowserOptions(SharedOptions sharedOptions)
    : base(sharedOptions)
  {
  }

  /// <summary>
  /// 目录格式化提供,默认是提供表格的形式展示,课自定义
  /// </summary>
  public IDirectoryFormatter Formatter { get; set; }
}

无独有偶这个类和StaticFileOptions一样也是集成自SharedOptionsBase类,唯一多了IDirectoryFormatter操作,通过它我们可以自定义展示到页面的输出形式,接下来我们就重点看下DirectoryBrowserMiddleware中间件的实现

public class DirectoryBrowserMiddleware
{
  private readonly DirectoryBrowserOptions _options;
  private readonly PathString _matchUrl;
  private readonly RequestDelegate _next;
  private readonly IDirectoryFormatter _formatter;
  private readonly IFileProvider _fileProvider;

  public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options)
    : this(next, hostingEnv, HtmlEncoder.Default, options)
  {
  }

  public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
  {
    _next = next;
    _options = options.Value;
    //默认是提供默认目录的访问程序
    _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
    //默认传递的是HtmlDirectoryFormatter类型,也就是我们看到的输出表格的页面
    _formatter = options.Value.Formatter ?? new HtmlDirectoryFormatter(encoder);
    _matchUrl = _options.RequestPath;
  }

  public Task Invoke(HttpContext context)
  {
    //1.IsGetOrHeadMethod判断是否为Get或Head请求
    //2.TryMatchPath判断请求的路径和设置的路径是否可以匹配的上
    //3.TryGetDirectoryInfo判断根据匹配出来的路径能否查找到真实的物理路径
    if (context.GetEndpoint() == null &&
      Helpers.IsGetOrHeadMethod(context.Request.Method)
      && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)
      && TryGetDirectoryInfo(subpath, out var contents))
    {
      //判断请求路径是否是/为结尾
      if (!Helpers.PathEndsInSlash(context.Request.Path))
      {
        //如果不是以斜线结尾则重定向(个人感觉直接在服务端重定向就可以了,为啥还要返回浏览器在请求一次)
        context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
        var request = context.Request;
        var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);
        context.Response.Headers[HeaderNames.Location] = redirect;
        return Task.CompletedTask;
      }
      //返回展示目录的内容
      return _formatter.GenerateContentAsync(context, contents);
    }
    return _next(context);
  }

  /// <summary>
  /// 根据请求路径匹配到物理路径信息是否存在,存在则返回路径信息
  /// </summary>
  private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents)
  {
    contents = _fileProvider.GetDirectoryContents(subpath.Value);
    return contents.Exists;
  }
}

这个操作相对简单了许多,主要就是判断请求路径能否和预设置的路径匹配的到,如果匹配到则获取可以操作当前目录内容IDirectoryContents然后通过IDirectoryFormatter输出如何展示目录内容,关于IDirectoryFormatter的默认实现类HtmlDirectoryFormatter这里就不展示里面的代码了,逻辑非常的加单就是拼接成table的html代码然后输出,有兴趣的同学可自行查看源码[点击查看HtmlDirectoryFormatter源码],如果自定义的话规则也非常简单,主要看你想输出啥

public class TreeDirectoryFormatter: IDirectoryFormatter
{
  public Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents)
  {
    //遍历contents实现你想展示的方式
  }
}

然后在UseDirectoryBrowser的时候给Formatter赋值即可

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
  Formatter = new TreeDirectoryFormatter()
});

UseDefaultFiles

很多时候出于安全考虑或者其他原因我们想在访问某个目录的时候返回一个默认的页面或展示,这个事实我们就需要使用UseDefaultFiles中间件,当我们配置了这个中间件,如果命中了配置路径,那么会直接返回默认的页面信息,简单使用方式如下

//wwwroot目录访问展示默认文件
app.UseDefaultFiles();
//或自定义目录默认展示文件
var fileProvider = new PhysicalFileProvider($"{env.ContentRootPath}/staticfiles");
app.UseDefaultFiles(new DefaultFilesOptions
{
  RequestPath = "/staticfiles",
  FileProvider = fileProvider
});

老规矩,我们查看下注册UseDefaultFiles的源码[点击查看DefaultFilesExtensions源码]

public static class DefaultFilesExtensions
{
  public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app)
  {
    return app.UseMiddleware<DefaultFilesMiddleware>();
  }

  public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath)
  {
    return app.UseDefaultFiles(new DefaultFilesOptions
    {
      RequestPath = new PathString(requestPath)
    });
  }

  public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options)
  {
    return app.UseMiddleware<DefaultFilesMiddleware>(Options.Create(options));
  }
}

使用方式和UseStaticFiles、UseDirectoryBrowser是一样,最终都是调用传递DefaultFilesOptions的方法,我们查看一下DefaultFilesOptions的大致实现[点击查看源码]

public class DefaultFilesOptions : SharedOptionsBase
{
  public DefaultFilesOptions()
    : this(new SharedOptions())
  {
  }

  public DefaultFilesOptions(SharedOptions sharedOptions)
    : base(sharedOptions)
  {
    //系统提供的默认页面的名称
    DefaultFileNames = new List<string>
    {
      "default.htm",
      "default.html",
      "index.htm",
      "index.html",
    };
  }

  /// <summary>
  /// 通过这个属性可以配置默认文件名称
  /// </summary>
  public IList<string> DefaultFileNames { get; set; }
}

和之前的方法如出一辙,都是继承自SharedOptionsBase,通过DefaultFileNames我们可以配置默认文件的名称,默认是default.html/htm和index.html/htm。我们直接查看中间件DefaultFilesMiddleware的源码[点击查看源码]

public class DefaultFilesMiddleware
{
  private readonly DefaultFilesOptions _options;
  private readonly PathString _matchUrl;
  private readonly RequestDelegate _next;
  private readonly IFileProvider _fileProvider;

  public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options)
  {
    _next = next;
    _options = options.Value;
    _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
    _matchUrl = _options.RequestPath;
  }

  public Task Invoke(HttpContext context)
  {
    //1.我们使用UseDefaultFiles中间件的时候要置于UseRouting之上,否则就会不生效
    //2.IsGetOrHeadMethod判断请求为Get或Head的情况下才生效
    //3.TryMatchPath判断请求的路径和设置的路径是否可以匹配的上
    if (context.GetEndpoint() == null &&
      Helpers.IsGetOrHeadMethod(context.Request.Method)
      && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath))
    {
      //根据匹配路径获取物理路径对应的信息
      var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);
      if (dirContents.Exists)
      {
        //循环配置的默认文件名称
        for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++)
        {
          string defaultFile = _options.DefaultFileNames[matchIndex];
          //匹配配置的启用默认文件的路径+遍历到的默认文件名称的路径是否存在
          var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile);
          if (file.Exists)
          {
            //判断请求路径是否已"/"结尾,如果不是则从定向(这个点个人感觉可以改进)
            if (!Helpers.PathEndsInSlash(context.Request.Path))
            {
              context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
              var request = context.Request;
              var redirect = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path + "/", request.QueryString);
              context.Response.Headers[HeaderNames.Location] = redirect;
              return Task.CompletedTask;
            }
            //如果匹配的上,则将配置的启用默认文件的路径+遍历到的默认文件名称的路径组合成新的Path交给_next(context)
            //比如将组成类似这种路径/staticfiles/index.html向下传递
            context.Request.Path = new PathString(context.Request.Path.Value + defaultFile);
            break;
          }
        }
      }
    }
    return _next(context);
  }
}

这个中间件的实现思路也非常简单主要的工作就是,匹配配置的启用默认文件的路径+遍历到的默认文件名称的路径是否存在,如果匹配的上,则将配置的启用默认文件的路径+遍历到的默认文件名称的路径组合成新的Path(比如/staticfiles/index.html)交给后续的中间件去处理。这里值得注意的是UseDefaultFiles 必须要配合UseStaticFiles一起使用,而且注册位置要出现在UseStaticFiles之上。这也是为什么UseDefaultFiles只需要匹配到默认文件所在的路径并重新赋值给context.Request.Path既可的原因。
当然我们也可以自定义默认文件的名称,因为只要能匹配的到具体的文件既可

var defaultFilesOptions = new DefaultFilesOptions
{
  RequestPath = "/staticfiles",
  FileProvider = fileProvider
};
//我们可以清除掉系统默认的默认文件名称
defaultFilesOptions.DefaultFileNames.Clear();
defaultFilesOptions.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(defaultFilesOptions);

总结

通过上面的介绍我们已经大致了解了静态文件处理的大致实现思路,相对于传统的Asp.Net程序我们可以更方便的处理静态文件信息,但是思路是一致的,IIS会优先处理静态文件,如果静态文件处理不了的情况才会交给程序去处理。ASP.NET Core也不例外,通过我们查看中间件源码里的context.GetEndpoint()==null判断可以知道,ASP.NET Core更希望我们优先去处理静态文件,而不是任意出现在其他位置去处理。关于ASP.NET Core处理静态文件的讲解就到这里,欢迎评论区探讨交流。

到此这篇关于浅谈ASP.NET Core静态文件处理源码探究的文章就介绍到这了,更多相关ASP.NET Core静态文件处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ASP.NET Core静态文件使用教程(9)

    在这一章,我们将学习如何使用文件.几乎每个web应用程序都需要一个重要特性:能够从文件系统提供文件(静态文件). 静态文件像JavaScript文件.图片.CSS文件等,我们Asp.Net Core应用程序可以直接提供给客户. 静态文件通常位于web根(wwwroot)文件夹. 默认情况下,这是我们可以直接从文件系统提供文件的唯一的地方.  案例 现在让我们通过一个简单的示例来了解我们在我们的应用程序如何提供这些静态文件. 在这里,我们想要向我们的 FirstAppDemo 应用程序添加一个简单

  • ASP.NET Core静态文件的使用方法

    前言 静态文件(HTML,CSS,图片和Javascript之类的资源)会被ASP.NET Core应用直接提供给客户端. 静态文件通常位于网站根目录(web root) <content-root>/wwwroot文件夹下.通常会把项目的当前目录设置为Content root,这样项目的web root就可以在开发阶段被明确. public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.Cre

  • ASP.NET Core 应用程序中的静态文件中间件的实现

    在这篇文章中,我将向大家介绍,如何使用中间件组件来处理静态文件.这篇文章中,我们讨论下面几个问题: 在ASP.NET Core中,我们需要把静态文件存放在哪里? 在ASP.NET Core中 wwwroot文件夹是啥? 怎样在ASP.NET Core应用程序中,配置静态文件中间件? UseFileServer中间件的作用是什么? 最重要的特性之一就是;几乎所有的web应用程序都应该具备直接从文件系统存取静态文件的能力.ASP.NET Core能够直接从客户端获取应用程序的静态文件,比如:HTML

  • 解析如何利用一个ASP.NET Core应用来发布静态文件

    虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件.CSS样式文件和图片文件的请求.针对不同格式的静态文件请求的处理,ASP.NET Core为我们提供了三个中间件,它们将是本系列文章论述的重点.不过在针对对它们展开介绍之前,我们照理通过一些简单的实例来体验一下如何在一个ASP.NET Core应用中发布静态文件. 目录 一.以Web的形式读取文件 二.浏览目录内容 三.显示默认页面

  • ASP.NET Core中预压缩静态文件的方法步骤

    前言 Web应用程序的优化是非常重要,因为使用更少的CPU,占用更少的带宽可以减少项目的费用. 在ASP.NET Core中我们可以很容易的启用响应压缩,但是针对预压缩文件,就需要做一些额外的功能了. 这篇博客文章展示了如何在ASP.NET Core中预压缩静态文件. 下面话不多说了,来一起看看详细的介绍吧 为什么需要预压缩文件? 虽然在从服务器请求文件时, 我们可以动态压缩文件,但这意味这Web服务器需要做更多的额外工作. 其实只有在新的应用程序部署时才会更改要压缩的文件. 越好的压缩效果需要

  • 浅谈ASP.NET Core静态文件处理源码探究

    前言 静态文件(如 HTML.CSS.图像和 JavaScript)等是Web程序的重要组成部分.传统的ASP.NET项目一般都是部署在IIS上,IIS是一个功能非常强大的服务器平台,可以直接处理接收到的静态文件处理而不需要经过应用程序池处理,所以很多情况下对于静态文件的处理程序本身是无感知的.ASP.NET Core则不同,作为Server的Kestrel服务是宿主到程序上的,由宿主运行程序启动Server然后可以监听请求,所以通过程序我们直接可以处理静态文件相关.静态文件默认存储到项目的ww

  • 浅谈ASP.NET Core 中jwt授权认证的流程原理

    1,快速实现授权验证 什么是 JWT ?为什么要用 JWT ?JWT 的组成? 这些百度可以直接找到,这里不再赘述. 实际上,只需要知道 JWT 认证模式是使用一段 Token 作为认证依据的手段. 我们看一下 Postman 设置 Token 的位置. 那么,如何使用 C# 的 HttpClient 访问一个 JWT 认证的 WebAPI 呢? 下面来创建一个 ASP.NET Core 项目,尝试添加 JWT 验证功能. 1.1 添加 JWT 服务配置 在 Startup.cs 的 Confi

  • 浅谈beego默认处理静态文件性能低下的问题

    今天使用ab(apacheBench)测试了一下beego的性能. 3Kbytes动态文件,在i3上可以达到每秒1W次响应的性能. 但是在测试静态文件时,beego出现了问题. ab测试参数:100次请求,并发数5. 问题表现:70%的请求直接失败,连接断开. 按道理来说,一个web server框架,静态文件的性能,应该是高于动态文件性能的. 在动态文件性能达到1W/s的情况下,没理由静态文件性能这么低下. 然后查看了一下beego的源代码.发现beego在处理动态文件请求时,有缓存.而处理静

  • asp .net core静态文件资源的深入讲解

    前言 对静态资源的简单的一个概况,在<重新整理.net core 计1400篇>系列后面会深入. 正文 我们在加入中间件是这样写的: app.UseStaticFiles(); 默认是给wwwroot提供资源. 那么我访问https://localhost:44330/js/site.js 资源,就可以访问到. // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-a

  • 浅谈ASP.NET Core中间件实现分布式 Session

    1.1. 中间件原理 1.1.1. 什么是中间件 中间件是段代码用于处理请求和响应,通常多个中间件链接起来形成管道,由每个中间件自己来决定是否要调用下一个中间件. 1.1.2. 中间件执行过程 举一个示例来演示中间件的执行过程(分别有三个中间件:日志记录.权限验证和路由):当请求进入应用程序时,执行执行日志记录的中间件,它记录请求属性并调用链中的下一个中间件权限验证,如果权限验证通过则将控制权传递给下一个中间件,不通过则设置401 HTTP代码并返回响应,响应传递给日志中间件进行返回. 1.1.

  • 浅谈ASP.NET Core 2.0 布局页面(译)

    本文介绍了ASP.NET Core 2.0 布局页面,分享给大家,具体如下: 问题 如何在ASP.NET Core 2.0项目中共享可见元素.代码块和指令? 答案 新建一个空项目,首先添加GreetingService服务和UserViewModel模型: public interface IGreetingService { string Greet(string firstname, string surname); } public class GreetingService : IGre

  • 浅谈ASP.NET Core 2.0 中间件(译)

    问题 如何创建一个最简单的ASP.NET Core中间件? 答案 使用VS创建一个ASP.NET Core 2.0的空项目,注意Startup.cs中的Configure()方法: public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.Run(async (context) => { await context.Response.WriteAsync("Hello World! (Run)

  • 浅谈ASP.Net Core WebApi几种版本控制对比

    一.版本控制的好处: (1)有助于及时推出功能, 而不会破坏现有系统. (2)它还可以帮助为选定的客户提供额外的功能. API 版本控制可以采用不同的方式进行控制,方法如下: (1)在 URL 中追加版本或作为查询字符串参数, (2)通过自定义标头和通过接受标头 在这篇文章中, 让我们来看看如何支持多个版本的 ASP.NET Core Web API. 一.创建asp.net core webapi 项目,引用NuGet包:Install-Package Microsoft.AspNetCore

随机推荐