ASP.NET Core静态文件处理源码探究
前言
????靜態文件(如 HTML、CSS、圖像和 JavaScript)等是Web程序的重要組成部分。傳統的ASP.NET項目一般都是部署在IIS上,IIS是一個功能非常強大的服務器平臺,可以直接處理接收到的靜態文件處理而不需要經過應用程序池處理,所以很多情況下對于靜態文件的處理程序本身是無感知的。ASP.NET Core則不同,作為Server的Kestrel服務是宿主到程序上的,由宿主運行程序啟動Server然后可以監聽請求,所以通過程序我們直接可以處理靜態文件相關。靜態文件默認存儲到項目的wwwroot目錄中,當然我們也可以自定義任意目錄去處理靜態文件??傊?#xff0c;在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,如果匹配到則返回contentTypeelse 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;//再次判斷請求HttpMethodif (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;//設置輸出壓縮相關headerSetCompressionMode();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源碼]
這個中間件啟用的重載方法和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既可的原因。
當然我們也可以自定義默認文件的名稱,因為只要能匹配的到具體的文件既可
總結
????通過上面的介紹我們已經大致了解了靜態文件處理的大致實現思路,相對于傳統的Asp.Net程序我們可以更方便的處理靜態文件信息,但是思路是一致的,IIS會優先處理靜態文件,如果靜態文件處理不了的情況才會交給程序去處理。ASP.NET Core也不例外,通過我們查看中間件源碼里的context.GetEndpoint()==null判斷可以知道,ASP.NET Core更希望我們優先去處理靜態文件,而不是任意出現在其他位置去處理。關于ASP.NET Core處理靜態文件的講解就到這里,歡迎評論區探討交流。
????歡迎掃碼關注我的公眾號????
總結
以上是生活随笔為你收集整理的ASP.NET Core静态文件处理源码探究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core CLI 的性能诊断工
- 下一篇: .NET 开源项目 StreamJson