加载_Layout文件ViewData的最佳实践:ActionFilter、ViewComponent还是其他?
针对你提到的_Layout页面加载通用站点数据的需求,我来拆解几种主流方案的优劣势,帮你找到最适合的实践:
如果你希望所有页面都能共享这些全局数据(站点名称、元数据等),全局ActionFilter是个很直接的选择。它能在每个Action执行前自动加载数据到ViewData,不用在每个控制器里重复写代码。
怎么实现?
你可以创建一个自定义的ActionFilter,在里面注入数据服务和缓存组件,一次性加载所有需要的全局配置,然后存入ViewData。举个代码例子:
public class LoadSiteGlobalDataFilter : IActionFilter { private readonly ISiteConfigService _configService; private readonly IMemoryCache _cache; public LoadSiteGlobalDataFilter(ISiteConfigService configService, IMemoryCache cache) { _configService = configService; _cache = cache; } public void OnActionExecuting(ActionExecutingContext context) { // 用缓存避免重复SQL查询 var globalSettings = _cache.GetOrCreate("SiteGlobalSettings", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); // 设置缓存过期时间 return _configService.GetGlobalSettings(); }); // 把数据存入ViewData,供_Layout使用 context.Controller.ViewData["SiteName"] = globalSettings.SiteName; context.Controller.ViewData["MetaKeywords"] = globalSettings.MetaKeywords; context.Controller.ViewData["MetaDescription"] = globalSettings.MetaDescription; context.Controller.ViewData["FacebookLink"] = globalSettings.FacebookLink; } public void OnActionExecuted(ActionExecutedContext context) { } }
然后在Startup里注册这个全局过滤器,让它对所有控制器生效:
services.AddControllersWithViews(options => { options.Filters.Add<LoadSiteGlobalDataFilter>(); });
优缺点
- ✅ 优点:全局统一处理,无需重复代码,所有页面直接用ViewData取数
- ❌ 缺点:如果部分页面不需要这些数据,会造成冗余;数据和渲染耦合在一起,不够灵活
如果你的数据是分模块使用的(比如头部的Facebook链接、页脚的联系信息是分开渲染的),ViewComponent会更合适。它把数据加载和视图渲染封装成独立组件,职责单一,还能单独缓存每个组件的输出。
怎么实现?
比如针对页脚联系信息,创建一个ViewComponent:
public class FooterContactViewComponent : ViewComponent { private readonly ISiteConfigService _configService; private readonly IMemoryCache _cache; public FooterContactViewComponent(ISiteConfigService configService, IMemoryCache cache) { _configService = configService; _cache = cache; } public async Task<IViewComponentResult> InvokeAsync() { // 单独缓存页脚数据 var contactInfo = await _cache.GetOrCreateAsync("FooterContactInfo", async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2); return await _configService.GetFooterContactInfo(); }); return View(contactInfo); } }
然后在_Layout的页脚部分调用这个组件:
<vc:footer-contact></vc:footer-contact>
同样,你可以为头部的社交链接、元数据分别创建对应的ViewComponent,按需调用。
优缺点
- ✅ 优点:组件化设计,维护方便;可以针对不同组件设置不同缓存策略;不需要全局注入,按需使用
- ❌ 缺点:如果有很多全局数据,需要创建多个组件,稍微有点繁琐
这是比较传统的做法:创建一个BaseController,在它的OnActionExecuting方法里加载全局数据,然后让所有业务控制器继承这个基类。
代码示例
public class BaseController : Controller { protected readonly ISiteConfigService _configService; protected readonly IMemoryCache _cache; public BaseController(ISiteConfigService configService, IMemoryCache cache) { _configService = configService; _cache = cache; } protected override void OnActionExecuting(ActionExecutingContext context) { var globalSettings = _cache.GetOrCreate("SiteGlobalSettings", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return _configService.GetGlobalSettings(); }); ViewData["SiteName"] = globalSettings.SiteName; // 其他数据... base.OnActionExecuting(context); } }
然后你的HomeController、ProductController等都继承BaseController即可。
优缺点
- ✅ 优点:实现简单,符合传统MVC开发习惯
- ❌ 缺点:继承链会限制控制器的灵活性;同样存在冗余问题,不需要这些数据的控制器也会加载
结合你的需求(加载多个全局配置、避免重复SQL),我推荐两种最优组合:
- 全局元数据用ActionFilter + 缓存:站点名称、元关键词/描述这类所有页面都需要的全局数据,用全局ActionFilter一次性加载并缓存,效率最高。
- 分模块数据用ViewComponent + 缓存:页脚联系信息、头部社交链接这类分区域渲染的数据,拆成独立的ViewComponent,各自缓存,维护更灵活。
不管选哪种方案,缓存都是核心——一定要把从数据库查询到的配置数据缓存起来,设置合理的过期时间(比如1-2小时),如果后台有配置修改功能,记得在修改后主动清除缓存,保证数据实时性。
内容的提问来源于stack exchange,提问作者user1085907

