当前位置 博文首页 > 文章内容

    ASP.NET Core MVC 修改视图的默认路径及其实现原理解析

    作者:shunshunshun18 栏目:未分类 时间:2021-09-19 14:44:32

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    本章将和大家分享如何在ASP.NET Core MVC中修改视图的默认路径,以及它的实现原理。

    导语:在日常工作过程中你可能会遇到这样的一种需求,就是在访问同一个页面时PC端和移动端显示的内容和风格是不一样(类似两个不一样的主题),但是它们的后端代码又是差不多的,此时我们就希望能够使用同一套后端代码,然后由系统自动去判断到底是PC端访问还是移动端访问,如果是移动端访问就优先匹配移动端的视图,在没有匹配到的情况下才去匹配PC端的视图。

    下面我们就来看下这个功能要如何实现,Demo的目录结构如下所示:

    本Demo的Web项目为ASP.NET Core Web 应用程序(目标框架为.NET Core 3.1) MVC项目。

    首先需要去扩展视图的默认路径,如下所示:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc.Razor;
    
    namespace NETCoreViewLocationExpander.ViewLocationExtend
    {
        /// <summary>
        /// 视图默认路径扩展
        /// </summary>
        public class TemplateViewLocationExpander : IViewLocationExpander
        {
            /// <summary>
            /// 扩展视图默认路径(PS:并非每次请求都会执行该方法)
            /// </summary>
            /// <param name="context"></param>
            /// <param name="viewLocations"></param>
            /// <returns></returns>
            public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
            {
                var template = context.Values["template"] ?? TemplateEnum.Default.ToString();
                if (template == TemplateEnum.WeChatArea.ToString())
                {
                    string[] weChatAreaViewLocationFormats = {
                        "/Areas/{2}/WeChatViews/{1}/{0}.cshtml",
                        "/Areas/{2}/WeChatViews/Shared/{0}.cshtml",
                        "/WeChatViews/Shared/{0}.cshtml"
                    };
                    //weChatAreaViewLocationFormats值在前--优先查找weChatAreaViewLocationFormats(即优先查找移动端目录)
                    return weChatAreaViewLocationFormats.Union(viewLocations);
                }
                else if (template == TemplateEnum.WeChat.ToString())
                {
                    string[] weChatViewLocationFormats = {
                        "/WeChatViews/{1}/{0}.cshtml",
                        "/WeChatViews/Shared/{0}.cshtml"
                    };
                    //weChatViewLocationFormats值在前--优先查找weChatViewLocationFormats(即优先查找移动端目录)
                    return weChatViewLocationFormats.Union(viewLocations);
                }
    
                return viewLocations;
            }
    
            /// <summary>
            /// 往ViewLocationExpanderContext.Values里面添加键值对(PS:每次请求都会执行该方法)
            /// </summary>
            /// <param name="context"></param>
            public void PopulateValues(ViewLocationExpanderContext context)
            {
                var userAgent = context.ActionContext.HttpContext.Request.Headers["User-Agent"].ToString();
                var isMobile = IsMobile(userAgent);
                var template = TemplateEnum.Default.ToString();
                if (isMobile)
                {
                    var areaName = //区域名称
                        context.ActionContext.RouteData.Values.ContainsKey("area")
                        ? context.ActionContext.RouteData.Values["area"].ToString()
                        : "";
                    var controllerName = //控制器名称
                        context.ActionContext.RouteData.Values.ContainsKey("controller")
                        ? context.ActionContext.RouteData.Values["controller"].ToString()
                        : "";
                    if (!string.IsNullOrEmpty(areaName) &&
                        !string.IsNullOrEmpty(controllerName)) //访问的是区域
                    {
                        template = TemplateEnum.WeChatArea.ToString();
                    }
                    else
                    {
                        template = TemplateEnum.WeChat.ToString();
                    }
                }
    
                context.Values["template"] = template; //context.Values会参与ViewLookupCache缓存Key(cacheKey)的生成
            }
    
            /// <summary>
            /// 判断是否是移动端
            /// </summary>
            /// <param name="userAgent"></param>
            /// <returns></returns>
            protected bool IsMobile(string userAgent)
            {
                userAgent = userAgent.ToLower();
                if (userAgent == "" ||
                    userAgent.IndexOf("mobile") > -1 ||
                    userAgent.IndexOf("mobi") > -1 ||
                    userAgent.IndexOf("nokia") > -1 ||
                    userAgent.IndexOf("samsung") > -1 ||
                    userAgent.IndexOf("sonyericsson") > -1 ||
                    userAgent.IndexOf("mot") > -1 ||
                    userAgent.IndexOf("blackberry") > -1 ||
                    userAgent.IndexOf("lg") > -1 ||
                    userAgent.IndexOf("htc") > -1 ||
                    userAgent.IndexOf("j2me") > -1 ||
                    userAgent.IndexOf("ucweb") > -1 ||
                    userAgent.IndexOf("opera mini") > -1 ||
                    userAgent.IndexOf("android") > -1 ||
                    userAgent.IndexOf("transcoder") > -1)
                {
                    return true;
                }
    
                return false;
            }
        }
    
        /// <summary>
        /// 模板枚举
        /// </summary>
        public enum TemplateEnum
        {
            Default = 1,
            WeChat = 2,
            WeChatArea = 3
        }
    }

    接着修改Startup.cs类,如下所示:

    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Razor;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    using NETCoreViewLocationExpander.ViewLocationExtend;
    
    namespace NETCoreViewLocationExpander
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllersWithViews();
    
                services.Configure<RazorViewEngineOptions>(options =>
                {
                    options.ViewLocationExpanders.Add(new TemplateViewLocationExpander()); //视图默认路径扩展
                });
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                }
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllerRoute(
                        name: "areas",
                        pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
    
                    endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");
                });
            }
        }
    }

    此外,Demo中还准备了两套视图:

    其中PC端视图如下所示:

    其中移动端视图如下所示:

    最后,我们分别使用PC端和移动端 来访问相关页面,如下所示:

    1、访问 /App/Home/Index 页面

    使用PC端访问,运行结果如下:

    使用移动端访问,运行结果如下:

    此时没有对应的移动端视图,所以都返回PC端的视图内容。

    2、访问 /App/Home/WeChat 页面

    使用PC端访问,运行结果如下:

    使用移动端访问,运行结果如下:

    此时有对应的移动端视图,所以当使用移动端访问时返回的是移动端的视图内容,而使用PC端访问时返回的则是PC端的视图内容。

    下面我们结合ASP.NET Core源码来分析下其实现原理:

    ASP.NET Core源码下载地址:https://github.com/dotnet/aspnetcore

    点击Source code下载,下载完成后,点击Release:

    可以将这个extensions源码一起下载下来,下载完成后如下所示:

    解压后我们重点来关注Razor视图引擎(RazorViewEngine.cs):

    RazorViewEngine.cs 源码如下所示:

    // Copyright (c) .NET Foundation. All rights reserved.
    // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
    
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Text.Encodings.Web;
    using Microsoft.AspNetCore.Mvc.Routing;
    using Microsoft.AspNetCore.Mvc.ViewEngines;
    using Microsoft.Extensions.Caching.Memory;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using Microsoft.Extensions.Primitives;
    
    namespace Microsoft.AspNetCore.Mvc.Razor
    {
        /// <summary>
        /// Default implementation of <see cref="IRazorViewEngine"/>.
        /// </summary>
        /// <remarks>
        /// For <c>ViewResults</c> returned from controllers, views should be located in
        /// <see cref="RazorViewEngineOptions.ViewLocationFormats"/>
        /// by default. For the controllers in an area, views should exist in
        /// <see cref="RazorViewEngineOptions.AreaViewLocationFormats"/>.
        /// </remarks>
        public class RazorViewEngine : IRazorViewEngine
        {
            public static readonly string ViewExtension = ".cshtml";
    
            private const string AreaKey = "area";
            private const string ControllerKey = "controller";
            private const string PageKey = "page";
    
            private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
    
            private readonly IRazorPageFactoryProvider _pageFactory;
            private readonly IRazorPageActivator _pageActivator;
            private readonly HtmlEncoder _htmlEncoder;
            private readonly ILogger _logger;
            private readonly RazorViewEngineOptions _options;
            private readonly DiagnosticListener _diagnosticListener;
    
            /// <summary>
            /// Initializes a new instance of the <see cref="RazorViewEngine" />.
            /// </summary>
            public RazorViewEngine(
                IRazorPageFactoryProvider pageFactory,
                IRazorPageActivator pageActivator,
                HtmlEncoder htmlEncoder,
                IOptions<RazorViewEngineOptions> optionsAccessor,
                ILoggerFactory loggerFactory,
                DiagnosticListener diagnosticListener)
            {
                _options = optionsAccessor.Value;
    
                if (_options.ViewLocationFormats.Count == 0)
                {
                    throw new ArgumentException(
                        Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.ViewLocationFormats)),
                        nameof(optionsAccessor));
                }
    
                if (_options.AreaViewLocationFormats.Count == 0)
                {
                    throw new ArgumentException(
                        Resources.FormatViewLocationFormatsIsRequired(nameof(RazorViewEngineOptions.AreaViewLocationFormats)),
                        nameof(optionsAccessor));
                }
    
                _pageFactory = pageFactory;
                _pageActivator = pageActivator;
                _htmlEncoder = htmlEncoder;
                _logger = loggerFactory.CreateLogger<RazorViewEngine>();
                _diagnosticListener = diagnosticListener;
                ViewLookupCache = new MemoryCache(new MemoryCacheOptions());
            }
    
            /// <summary>
            /// A cache for results of view lookups.
            /// </summary>
            protected IMemoryCache ViewLookupCache { get; }
    
            /// <summary>
            /// Gets the case-normalized route value for the specified route <paramref name="key"/>.
            /// </summary>
            /// <param name="context">The <see cref="ActionContext"/>.</param>
            /// <param name="key">The route key to lookup.</param>
            /// <returns>The value corresponding to the key.</returns>
            /// <remarks>
            /// The casing of a route value in <see cref="ActionContext.RouteData"/> is determined by the client.
            /// This making constructing paths for view locations in a case sensitive file system unreliable. Using the
            /// <see cref="Abstractions.ActionDescriptor.RouteValues"/> to get route values
            /// produces consistently cased results.
            /// </remarks>
            public static string GetNormalizedRouteValue(ActionContext context, string key)
                => NormalizedRouteValue.GetNormalizedRouteValue(context, key);
    
            /// <inheritdoc />
            public RazorPageResult FindPage(ActionContext context, string pageName)
            {
                if (context == null)
                {
                    throw new ArgumentNullException(nameof(context));
                }
    
                if (string.IsNullOrEmpty(pageName))
                {
                    throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
                }
    
                if (IsApplicationRelativePath(pageName) || IsRelativePath(pageName))
                {
                    // A path; not a name this method can handle.
                    return new RazorPageResult(pageName, Enumerable.Empty<string>());
                }
    
                var cacheResult = LocatePageFromViewLocations(context, pageName, isMainPage: false);
                if (cacheResult.Success)
                {
                    var razorPage = cacheResult.ViewEntry.PageFactory();
                    return new RazorPageResult(pageName, razorPage);
                }
                else
                {
                    return new RazorPageResult(pageName, cacheResult.SearchedLocations);
                }
            }
    
            /// <inheritdoc />
            public RazorPageResult GetPage(string executingFilePath, string pagePath)
            {
                if (string.IsNullOrEmpty(pagePath))
                {
                    throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pagePath));
                }
    
                if (!(IsApplicationRelativePath(pagePath) || IsRelativePath(pagePath)))
                {
                    // Not a path this method can handle.
                    return new RazorPageResult(pagePath, Enumerable.Empty<string>());
                }
    
                var cacheResult = LocatePageFromPath(executingFilePath, pagePath, isMainPage: false);
                if (cacheResult.Success)
                {
                    var razorPage = cacheResult.ViewEntry.PageFactory();
                    return new RazorPageResult(pagePath, razorPage);
                }
                else
                {
                    return new RazorPageResult(pagePath, cacheResult.SearchedLocations);
                }
            }
    
            /// <inheritdoc />
            public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
            {
                if (context == null)
                {
                    throw new ArgumentNullException(nameof(context));
                }
    
                if (string.IsNullOrEmpty(viewName))
                {
                    throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
                }
    
                if (IsApplicationRelativePath(viewName) || IsRelativePath(viewName))
                {
                    // A path; not a name this method can handle.
                    return ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>());
                }
    
                var cacheResult = LocatePageFromViewLocations(context, viewName, isMainPage);
                return CreateViewEngineResult(cacheResult, viewName);
            }
    
            /// <inheritdoc />
            public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
            {
                if (string.IsNullOrEmpty(viewPath))
                {
                    throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewPath));
                }
    
                if (!(IsApplicationRelativePath(viewPath) || IsRelativePath(viewPath)))
                {
                    // Not a path this method can handle.
                    return ViewEngineResult.NotFound(viewPath, Enumerable.Empty<string>());
                }
    
                var cacheResult = LocatePageFromPath(executingFilePath, viewPath, isMainPage);
                return CreateViewEngineResult(cacheResult, viewPath);
            }
    
            private ViewLocationCacheResult LocatePageFromPath(string executingFilePath, string pagePath, bool isMainPage)
            {
                var applicationRelativePath = GetAbsolutePath(executingFilePath, pagePath);
                var cacheKey = new ViewLocationCacheKey(applicationRelativePath, isMainPage);
                if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
                {
                    var expirationTokens = new HashSet<IChangeToken>();
                    cacheResult = CreateCacheResult(expirationTokens, applicationRelativePath, isMainPage);
    
                    var cacheEntryOptions = new MemoryCacheEntryOptions();
                    cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
                    foreach (var expirationToken in expirationTokens)
                    {
                        cacheEntryOptions.AddExpirationToken(expirationToken);
                    }
    
                    // No views were found at the specified location. Create a not found result.
                    if (cacheResult == null)
                    {
                        cacheResult = new ViewLocationCacheResult(new[] { applicationRelativePath });
                    }
    
                    cacheResult = ViewLookupCache.Set(
                        cacheKey,
                        cacheResult,
                        cacheEntryOptions);
                }
    
                return cacheResult;
            }
    
            private ViewLocationCacheResult LocatePageFromViewLocations(
                ActionContext actionContext,
                string pageName,
                bool isMainPage)
            {
                var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
                var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
                string razorPageName = null;
                if (actionContext.ActionDescriptor.RouteValues.ContainsKey(PageKey))
                {
                    // Only calculate the Razor Page name if "page" is registered in RouteValues.
                    razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
                }
    
                var expanderContext = new ViewLocationExpanderContext(
                    actionContext,
                    pageName,
                    controllerName,
                    areaName,
                    razorPageName,
                    isMainPage);
                Dictionary<string, string> expanderValues = null;
    
                var expanders = _options.ViewLocationExpanders;
                // Read interface .Count once rather than per iteration
                var expandersCount = expanders.Count;
                if (expandersCount > 0)
                {
                    expanderValues = new Dictionary<string, string>(StringComparer.Ordinal);
                    expanderContext.Values = expanderValues;
    
                    // Perf: Avoid allocations
                    for (var i = 0; i < expandersCount; i++)
                    {
                        expanders[i].PopulateValues(expanderContext);
                    }
                }
    
                var cacheKey = new ViewLocationCacheKey(
                    expanderContext.ViewName,
                    expanderContext.ControllerName,
                    expanderContext.AreaName,
                    expanderContext.PageName,
                    expanderContext.IsMainPage,
                    expanderValues);
    
                if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
                {
                    _logger.ViewLookupCacheMiss(cacheKey.ViewName, cacheKey.ControllerName);
                    cacheResult = OnCacheMiss(expanderContext, cacheKey);
                }
                else
                {
                    _logger.ViewLookupCacheHit(cacheKey.ViewName, cacheKey.ControllerName);
                }
    
                return cacheResult;
            }
    
            /// <inheritdoc />
            public string GetAbsolutePath(string executingFilePath, string pagePath)
            {
                if (string.IsNullOrEmpty(pagePath))
                {
                    // Path is not valid; no change required.
                    return pagePath;
                }
    
                if (IsApplicationRelativePath(pagePath))
                {
                    // An absolute path already; no change required.
                    return pagePath;
                }
    
                if (!IsRelativePath(pagePath))
                {
                    // A page name; no change required.
                    return pagePath;
                }
    
                if (string.IsNullOrEmpty(executingFilePath))
                {
                    // Given a relative path i.e. not yet application-relative (starting with "~/" or "/"), interpret
                    // path relative to currently-executing view, if any.
                    // Not yet executing a view. Start in app root.
                    var absolutePath = "/" + pagePath;
                    return ViewEnginePath.ResolvePath(absolutePath);
                }
    
                return ViewEnginePath.CombinePath(executingFilePath, pagePath);
            }
    
            // internal for tests
            internal IEnumerable<string> GetViewLocationFormats(ViewLocationExpanderContext context)
            {
                if (!string.IsNullOrEmpty(context.AreaName) &&
                    !string.IsNullOrEmpty(context.ControllerName))
                {
                    return _options.AreaViewLocationFormats;
                }
                else if (!string.IsNullOrEmpty(context.ControllerName))
                {
                    return _options.ViewLocationFormats;
                }
                else if (!string.IsNullOrEmpty(context.AreaName) &&
                    !string.IsNullOrEmpty(context.PageName))
                {
                    return _options.AreaPageViewLocationFormats;
                }
                else if (!string.IsNullOrEmpty(context.PageName))
                {
                    return _options.PageViewLocationFormats;
                }
                else
                {
                    // If we don't match one of these conditions, we'll just treat it like regular controller/action
                    // and use those search paths. This is what we did in 1.0.0 without giving much thought to it.
                    return _options.ViewLocationFormats;
                }
            }
    
            private ViewLocationCacheResult OnCacheMiss(
                ViewLocationExpanderContext expanderContext,
                ViewLocationCacheKey cacheKey)
            {
                var viewLocations = GetViewLocationFormats(expanderContext);
    
                var expanders = _options.ViewLocationExpanders;
                // Read interface .Count once rather than per iteration
                var expandersCount = expanders.Count;
                for (var i = 0; i < expandersCount; i++)
                {
                    viewLocations = expanders[i].ExpandViewLocations(expanderContext, viewLocations);
                }
    
                ViewLocationCacheResult cacheResult = null;
                var searchedLocations = new List<string>();
                var expirationTokens = new HashSet<IChangeToken>();
                foreach (var location in viewLocations)
                {
                    var path = string.Format(
                        CultureInfo.InvariantCulture,
                        location,
                        expanderContext.ViewName,
                        expanderContext.ControllerName,
                        expanderContext.AreaName);
    
                    path = ViewEnginePath.ResolvePath(path);
    
                    cacheResult = CreateCacheResult(expirationTokens, path, expanderContext.IsMainPage);
                    if (cacheResult != null)
                    {
                        break;
                    }
    
                    searchedLocations.Add(path);
                }
    
                // No views were found at the specified location. Create a not found result.
                if (cacheResult == null)
                {
                    cacheResult = new ViewLocationCacheResult(searchedLocations);
                }
    
                var cacheEntryOptions = new MemoryCacheEntryOptions();
                cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
                foreach (var expirationToken in expirationTokens)
                {
                    cacheEntryOptions.AddExpirationToken(expirationToken);
                }
    
                return ViewLookupCache.Set(cacheKey, cacheResult, cacheEntryOptions);
            }
    
            // Internal for unit testing
            internal ViewLocationCacheResult CreateCacheResult(
                HashSet<IChangeToken> expirationTokens,
                string relativePath,
                bool isMainPage)
            {
                var factoryResult = _pageFactory.CreateFactory(relativePath);
                var viewDescriptor = factoryResult.ViewDescriptor;
                if (viewDescriptor?.ExpirationTokens != null)
                {
                    var viewExpirationTokens = viewDescriptor.ExpirationTokens;
                    // Read interface .Count once rather than per iteration
                    var viewExpirationTokensCount = viewExpirationTokens.Count;
                    for (var i = 0; i < viewExpirationTokensCount; i++)
                    {
                        expirationTokens.Add(viewExpirationTokens[i]);
                    }
                }
    
                if (factoryResult.Success)
                {
                    // Only need to lookup _ViewStarts for the main page.
                    var viewStartPages = isMainPage ?
                        GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
                        Array.Empty<ViewLocationCacheItem>();
    
                    return new ViewLocationCacheResult(
                        new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath),
                        viewStartPages);
                }
    
                return null;
            }
    
            private IReadOnlyList<ViewLocationCacheItem> GetViewStartPages(
                string path,
                HashSet<IChangeToken> expirationTokens)
            {
                var viewStartPages = new List<ViewLocationCacheItem>();
    
                foreach (var filePath in RazorFileHierarchy.GetViewStartPaths(path))
                {
                    var result = _pageFactory.CreateFactory(filePath);
                    var viewDescriptor = result.ViewDescriptor;
                    if (viewDescriptor?.ExpirationTokens != null)
                    {
                        for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++)
                        {
                            expirationTokens.Add(viewDescriptor.ExpirationTokens[i]);
                        }
                    }
    
                    if (result.Success)
                    {
                        // Populate the viewStartPages list so that _ViewStarts appear in the order the need to be
                        // executed (closest last, furthest first). This is the reverse order in which
                        // ViewHierarchyUtility.GetViewStartLocations returns _ViewStarts.
                        viewStartPages.Insert(0, new ViewLocationCacheItem(result.RazorPageFactory, filePath));
                    }
                }
    
                return viewStartPages;
            }
    
            private ViewEngineResult CreateViewEngineResult(ViewLocationCacheResult result, string viewName)
            {
                if (!result.Success)
                {
                    return ViewEngineResult.NotFound(viewName, result.SearchedLocations);
                }
    
                var page = result.ViewEntry.PageFactory();
    
                var viewStarts = new IRazorPage[result.ViewStartEntries.Count];
                for (var i = 0; i < viewStarts.Length; i++)
                {
                    var viewStartItem = result.ViewStartEntries[i];
                    viewStarts[i] = viewStartItem.PageFactory();
                }
    
                var view = new RazorView(this, _pageActivator, viewStarts, page, _htmlEncoder, _diagnosticListener);
                return ViewEngineResult.Found(viewName, view);
            }
    
            private static bool IsApplicationRelativePath(string name)
            {
                Debug.Assert(!string.IsNullOrEmpty(name));
                return name[0] == '~' || name[0] == '/';
            }
    
            private static bool IsRelativePath(string name)
            {
                Debug.Assert(!string.IsNullOrEmpty(name));
    
                // Though ./ViewName looks like a relative path, framework searches for that view using view locations.
                return name.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase);
            }
        }
    }
    

    我们从用于寻找视图的 FindView 方法开始阅读:

    /// <inheritdoc />
    public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
    
        if (string.IsNullOrEmpty(viewName))
        {
            throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
        }
    
        if (IsApplicationRelativePath(viewName) || IsRelativePath(viewName))
        {
            // A path; not a name this method can handle.
            return ViewEngineResult.NotFound(viewName, Enumerable.Empty<string>());
        }
    
        var cacheResult = LocatePageFromViewLocations(context, viewName, isMainPage);
        return CreateViewEngineResult(cacheResult, viewName);
    }

    接着定位找到LocatePageFromViewLocations 方法:

    private ViewLocationCacheResult LocatePageFromViewLocations(
        ActionContext actionContext,
        string pageName,
        bool isMainPage)
    {
        var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
        var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
        string razorPageName = null;
        if (actionContext.ActionDescriptor.RouteValues.ContainsKey(PageKey))
        {
            // Only calculate the Razor Page name if "page" is registered in RouteValues.
            razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
        }
    
        var expanderContext = new ViewLocationExpanderContext(
            actionContext,
            pageName,
            controllerName,
            areaName,
            razorPageName,
            isMainPage);
        Dictionary<string, string> expanderValues = null;
    
        var expanders = _options.ViewLocationExpanders;
        // Read interface .Count once rather than per iteration
        var expandersCount = expanders.Count;
        if (expandersCount > 0)
        {
            expanderValues = new Dictionary<string, string>(StringComparer.Ordinal);
            expanderContext.Values = expanderValues;
    
            // Perf: Avoid allocations
            for (var i = 0; i < expandersCount; i++)
            {
                expanders[i].PopulateValues(expanderContext);
            }
        }
    
        var cacheKey = new ViewLocationCacheKey(
            expanderContext.ViewName,
            expanderContext.ControllerName,
            expanderContext.AreaName,
            expanderContext.PageName,
            expanderContext.IsMainPage,
            expanderValues);
    
        if (!ViewLookupCache.TryGetValue(cacheKey, out ViewLocationCacheResult cacheResult))
        {
            _logger.ViewLookupCacheMiss(cacheKey.ViewName, cacheKey.ControllerName);
            cacheResult = OnCacheMiss(expanderContext, cacheKey);
        }
        else
        {
            _logger.ViewLookupCacheHit(cacheKey.ViewName, cacheKey.ControllerName);
        }
    
        return cacheResult;
    }

    从此处可以看出,每次查找视图的时候都会调用 ViewLocationExpander.PopulateValues 方法,并且最终的这个 expanderValues 会参与ViewLookupCache 缓存key(cacheKey)的生成。

    此外还可以看出,如果从 ViewLookupCache 这个缓存中能找到数据的话,它就直接返回了,不会再去调用ViewLocationExpander.ExpandViewLocations 方法。

    这也就解释了为什么我们Demo中是在 PopulateValues 方法里面去设置context.Values["template"] 的值,而不是直接在 ExpandViewLocations 方法里面去设置这个值。

    下面我们接着找到用于生成 cacheKey 的ViewLocationCacheKey 类,如下所示:

    // Copyright (c) .NET Foundation. All rights reserved.
    // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
    
    using System;
    using System.Collections.Generic;
    using Microsoft.Extensions.Internal;
    
    namespace Microsoft.AspNetCore.Mvc.Razor
    {
        /// <summary>
        /// Key for entries in <see cref="RazorViewEngine.ViewLookupCache"/>.
        /// </summary>
        internal readonly struct ViewLocationCacheKey : IEquatable<ViewLocationCacheKey>
        {
            /// <summary>
            /// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
            /// </summary>
            /// <param name="viewName">The view name or path.</param>
            /// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
            public ViewLocationCacheKey(
                string viewName,
                bool isMainPage)
                : this(
                      viewName,
                      controllerName: null,
                      areaName: null,
                      pageName: null,
                      isMainPage: isMainPage,
                      values: null)
            {
            }
    
            /// <summary>
            /// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
            /// </summary>
            /// <param name="viewName">The view name.</param>
            /// <param name="controllerName">The controller name.</param>
            /// <param name="areaName">The area name.</param>
            /// <param name="pageName">The page name.</param>
            /// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
            /// <param name="values">Values from <see cref="IViewLocationExpander"/> instances.</param>
            public ViewLocationCacheKey(
                string viewName,
                string controllerName,
                string areaName,
                string pageName,
                bool isMainPage,
                IReadOnlyDictionary<string, string> values)
            {
                ViewName = viewName;
                ControllerName = controllerName;
                AreaName = areaName;
                PageName = pageName;
                IsMainPage = isMainPage;
                ViewLocationExpanderValues = values;
            }
    
            /// <summary>
            /// Gets the view name.
            /// </summary>
            public string ViewName { get; }
    
            /// <summary>
            /// Gets the controller name.
            /// </summary>
            public string ControllerName { get; }
    
            /// <summary>
            /// Gets the area name.
            /// </summary>
            public string AreaName { get; }
    
            /// <summary>
            /// Gets the page name.
            /// </summary>
            public string PageName { get; }
    
            /// <summary>
            /// Determines if the page being found is the main page for an action.
            /// </summary>
            public bool IsMainPage { get; }
    
            /// <summary>
            /// Gets the values populated by <see cref="IViewLocationExpander"/> instances.
            /// </summary>
            public IReadOnlyDictionary<string, string> ViewLocationExpanderValues { get; }
    
            /// <inheritdoc />
            public bool Equals(ViewLocationCacheKey y)
            {
                if (IsMainPage != y.IsMainPage ||
                    !string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
                    !string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
                    !string.Equals(AreaName, y.AreaName, StringComparison.Ordinal) ||
                    !string.Equals(PageName, y.PageName, StringComparison.Ordinal))
                {
                    return false;
                }
    
                if (ReferenceEquals(ViewLocationExpanderValues, y.ViewLocationExpanderValues))
                {
                    return true;
                }
    
                if (ViewLocationExpanderValues == null ||
                    y.ViewLocationExpanderValues == null ||
                    (ViewLocationExpanderValues.Count != y.ViewLocationExpanderValues.Count))
                {
                    return false;
                }
    
                foreach (var item in ViewLocationExpanderValues)
                {
                    if (!y.ViewLocationExpanderValues.TryGetValue(item.Key, out var yValue) ||
                        !string.Equals(item.Value, yValue, StringComparison.Ordinal))
                    {
                        return false;
                    }
                }
    
                return true;
            }
    
            /// <inheritdoc />
            public override bool Equals(object obj)
            {
                if (obj is ViewLocationCacheKey)
                {
                    return Equals((ViewLocationCacheKey)obj);
                }
    
                return false;
            }
    
            /// <inheritdoc />
            public override int GetHashCode()
            {
                var hashCodeCombiner = HashCodeCombiner.Start();
                hashCodeCombiner.Add(IsMainPage ? 1 : 0);
                hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
                hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
                hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);
                hashCodeCombiner.Add(PageName, StringComparer.Ordinal);
    
                if (ViewLocationExpanderValues != null)
                {
                    foreach (var item in ViewLocationExpanderValues)
                    {
                        hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
                        hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
                    }
                }
    
                return hashCodeCombiner;
            }
        }
    }

    我们重点来看下其中的 Equals 方法,如下所示:

    /// <inheritdoc />
    public bool Equals(ViewLocationCacheKey y)
    {
        if (IsMainPage != y.IsMainPage ||
            !string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
            !string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
            !string.Equals(AreaName, y.AreaName, StringComparison.Ordinal) ||
            !string.Equals(PageName, y.PageName, StringComparison.Ordinal))
        {
            return false;
        }
    
        if (ReferenceEquals(ViewLocationExpanderValues, y.ViewLocationExpanderValues))
        {
            return true;
        }
    
        if (ViewLocationExpanderValues == null ||
            y.ViewLocationExpanderValues == null ||
            (ViewLocationExpanderValues.Count != y.ViewLocationExpanderValues.Count))
        {
            return false;
        }
    
        foreach (var item in ViewLocationExpanderValues)
        {
            if (!y.ViewLocationExpanderValues.TryGetValue(item.Key, out var yValue) ||
                !string.Equals(item.Value, yValue, StringComparison.Ordinal))
            {
                return false;
            }
        }
    
        return true;
    }

    从此处可以看出,如果 expanderValues 字典中 键/值对的数目不同或者其中任意一个值不同,那么这个 cacheKey 就是不同的。

    我们继续往下分析, 从上文中我们知道,如果从ViewLookupCache 缓存中没有找到数据,那么它就会执行OnCacheMiss 方法。

    我们找到OnCacheMiss 方法,如下所示:

    private ViewLocationCacheResult OnCacheMiss(
        ViewLocationExpanderContext expanderContext,
        ViewLocationCacheKey cacheKey)
    {
        var viewLocations = GetViewLocationFormats(expanderContext);
    
        var expanders = _options.ViewLocationExpanders;
        // Read interface .Count once rather than per iteration
        var expandersCount = expanders.Count;
        for (var i = 0; i < expandersCount; i++)
        {
            viewLocations = expanders[i].ExpandViewLocations(expanderContext, viewLocations);
        }
    
        ViewLocationCacheResult cacheResult = null;
        var searchedLocations = new List<string>();
        var expirationTokens = new HashSet<IChangeToken>();
        foreach (var location in viewLocations)
        {
            var path = string.Format(
                CultureInfo.InvariantCulture,
                location,
                expanderContext.ViewName,
                expanderContext.ControllerName,
                expanderContext.AreaName);
    
            path = ViewEnginePath.ResolvePath(path);
    
            cacheResult = CreateCacheResult(expirationTokens, path, expanderContext.IsMainPage);
            if (cacheResult != null)
            {
                break;
            }
    
            searchedLocations.Add(path);
        }
    
        // No views were found at the specified location. Create a not found result.
        if (cacheResult == null)
        {
            cacheResult = new ViewLocationCacheResult(searchedLocations);
        }
    
        var cacheEntryOptions = new MemoryCacheEntryOptions();
        cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
        foreach (var expirationToken in expirationTokens)
        {
            cacheEntryOptions.AddExpirationToken(expirationToken);
        }
    
        return ViewLookupCache.Set(cacheKey, cacheResult, cacheEntryOptions);
    }

    仔细观察之后你就会发现:

    1、首先它是通过GetViewLocationFormats 方法获取初始的 viewLocations视图位置集合。

    2、接着它会按顺序依次调用所有的ViewLocationExpander.ExpandViewLocations 方法,经过一系列聚合操作后得到最终的viewLocations 视图位置集合。

    3、然后遍历 viewLocations 视图位置集合,按顺序依次去指定的路径中查找对应的视图,只要找到符合条件的第一个视图就结束循环,不再往下查找,最后设置缓存返回结果。

    4、视图位置字符串(例如:“/Areas/{2}/WeChatViews/{1}/{0}.cshtml”)中的占位符含义:“{0}” 表示视图名称,“{1}” 表示控制器名称,“{2}” 表示区域名称。

    下面我们继续找到GetViewLocationFormats 方法,如下所示:

    // internal for tests
    internal IEnumerable<string> GetViewLocationFormats(ViewLocationExpanderContext context)
    {
        if (!string.IsNullOrEmpty(context.AreaName) &&
            !string.IsNullOrEmpty(context.ControllerName))
        {
            return _options.AreaViewLocationFormats;
        }
        else if (!string.IsNullOrEmpty(context.ControllerName))
        {
            return _options.ViewLocationFormats;
        }
        else if (!string.IsNullOrEmpty(context.AreaName) &&
            !string.IsNullOrEmpty(context.PageName))
        {
            return _options.AreaPageViewLocationFormats;
        }
        else if (!string.IsNullOrEmpty(context.PageName))
        {
            return _options.PageViewLocationFormats;
        }
        else
        {
            // If we don't match one of these conditions, we'll just treat it like regular controller/action
            // and use those search paths. This is what we did in 1.0.0 without giving much thought to it.
            return _options.ViewLocationFormats;
        }
    }

    从此处可以看出,它是通过判断 区域名称和控制器名称 是否都不为空,以此来判断客户端访问的到底是区域还是非区域。

    文章最后我们通过调试来看下AreaViewLocationFormats 和ViewLocationFormats 的初始值:

    至此本文就全部介绍完了,如果觉得对您有所启发请记得点个赞哦!!!

    Demo源码:

    链接: https://pan.baidu.com/s/1gn4JQTzn7hQLgfAtaUPDLg

    提取码: mjgr