在 ASP.NET Core 中将 Razor 视图渲染为字符串

Posted

技术标签:

【中文标题】在 ASP.NET Core 中将 Razor 视图渲染为字符串【英文标题】:Render Razor View to string in ASP.NET Core 【发布时间】:2015-09-14 07:05:37 【问题描述】:

我在 MVC 6 项目中使用 RazorEngine 解析模板,如下所示:

Engine.Razor.RunCompile(File.ReadAllText(fullTemplateFilePath), templateName, null, model);

它适用于 beta 6。它在升级到 beta 7 后无法正常工作,并出现以下错误:

MissingMethodException:找不到方法:“无效 Microsoft.AspNet.Razor.CodeGenerators.GeneratedClassContext.set_ResolveUrlMethodName(System.String)”。 在 RazorEngine.Compilation.CompilerServiceBase.CreateHost(Type templateType, Type modelType, String className)

这是 global.json:


  "projects": [ "src", "test" ],
  "sdk": 
    "version": "1.0.0-beta7",
    "runtime": "clr",
    "architecture": "x64"
  

这是 project.json:

...
"dependencies": 
    "EntityFramework.SqlServer": "7.0.0-beta7",
    "EntityFramework.Commands": "7.0.0-beta7",
    "Microsoft.AspNet.Mvc": "6.0.0-beta7",
    "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta7",
    "Microsoft.AspNet.Authentication.Cookies": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Facebook": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Google": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Twitter": "1.0.0-beta7",
    "Microsoft.AspNet.Diagnostics": "1.0.0-beta7",
    "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta7",
    "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta7",
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta7",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta7",
    "Microsoft.AspNet.StaticFiles": "1.0.0-beta7",
    "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.Abstractions": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.Json": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.UserSecrets": "1.0.0-beta7",
    "Microsoft.Framework.Logging": "1.0.0-beta7",
    "Microsoft.Framework.Logging.Console": "1.0.0-beta7",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta7",
    "RazorEngine": "4.2.2-beta1"
  ,
...
  "frameworks": 
    "dnx451":  
  ,
...

我的模板是:

@model dynamic
@
    Layout = null;


<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Registration</title>
</head>
<body>
<p>
    Hello, @Model
</p>
</body>
</html>

有没有人有类似的问题? 在 MVC 6 中还有另一种解析模板的方法吗?

【问题讨论】:

我没有读过这个问题,但投票赞成答案:) 【参考方案1】:

2016 年 7 月更新

在以下版本1.0.0RC2上工作正常


谁在瞄准 aspnetcore RC2,这个 sn-p 可能会帮助你:

创建一个单独的服务,以便在您不在控制器上下文中时使用它,例如从命令行或队列运行器等... 在您的 IoC 容器中的 Startup 类中注册此服务

https://gist.github.com/ahmad-moussawi/1643d703c11699a6a4046e57247b4d09

用法

// using a Model
string html = view.Render("Emails/Test", new Product("Apple"));

// using a Dictionary<string, object>
var viewData = new Dictionary<string, object>();
viewData["Name"] = "123456";

string html = view.Render("Emails/Test", viewData);

注意事项

Razor 中的链接呈现为 相对 URL,因此这不适用于外部视图(如电子邮件等)。

至于现在在控制器上生成链接并通过 ViewModel 将其传递给视图。

信用

来源摘自(感谢@pholly):https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs)

【讨论】:

Gist 与 github.com/aspnet/Entropy/blob/dev/samples/… 完全相同的代码是因为 github.com/aspnet/Mvc/issues/3091 而创建的 如果我在视图中使用链接 taghelper,我将无法正常工作:&lt;a asp-controller="Whatever" asp-action="DoSomething" asp-route-thingy="ThingyValue"&gt;link text&lt;/a&gt; @BenCollins,我假设你得到了一个错误的 URL(相对 URL),它在电子邮件等外部链接上不起作用,对吗? @Ahmad 不,问题是链接 taghelper 需要更多上下文才能工作。链接 taghelper 依赖于UrlHelper,而UrlHelper 需要真正的路由数据,否则会抛出异常。对我来说幸运的是,我在 MVC 应用程序中执行此操作,因此我可以使用 IHttpContextAccessor 构建具有实际值的 ActionContext 是的,我认为这也是我的问题,因为我需要在IUrlHelper 中添加扩展方法AbsoluteAction,很高兴您找到了解决方案:) 【参考方案2】:

我找到了讨论它的线程:https://github.com/aspnet/Mvc/issues/3091

线程中的某人在这里创建了一个示例服务:https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs

经过反复试验,我能够减少服务,因此它只需要一个有效的HttpContext 和一个ViewEngine,我添加了一个不需要模型的重载。视图相对于您的应用程序根目录(它们不必位于Views 文件夹中)。

您需要在Startup.cs 注册服务并同时注册HttpContextAccessor

//Startup.cs ConfigureServices()
services.AddTransient<ViewRenderService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.IO;

namespace LibraryApi.Services

    public class ViewRenderService
    
        IRazorViewEngine _viewEngine;
        IHttpContextAccessor _httpContextAccessor;

        public ViewRenderService(IRazorViewEngine viewEngine, IHttpContextAccessor httpContextAccessor)
        
            _viewEngine = viewEngine;
            _httpContextAccessor = httpContextAccessor;
        

        public string Render(string viewPath)
        
            return Render(viewPath, string.Empty);
        

        public string Render<TModel>(string viewPath, TModel model)
        
            var viewEngineResult = _viewEngine.GetView("~/", viewPath, false);

            if (!viewEngineResult.Success)
            
                throw new InvalidOperationException($"Couldn't find view viewPath");
            

            var view = viewEngineResult.View;

            using (var output = new StringWriter())
            
                var viewContext = new ViewContext();
                viewContext.HttpContext = _httpContextAccessor.HttpContext;
                viewContext.ViewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                 Model = model ;
                viewContext.Writer = output;

                view.RenderAsync(viewContext).GetAwaiter().GetResult();

                return output.ToString();
            
        
    

示例用法:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using LibraryApi.Services;
using System.Dynamic;

namespace LibraryApi.Controllers

    [Route("api/[controller]")]
    public class ValuesController : Controller
    
        ILogger<ValuesController> _logger;
        ViewRenderService _viewRender;
        public ValuesController(ILogger<ValuesController> logger, ViewRenderService viewRender)
        
            _logger = logger;
            _viewRender = viewRender;
        

        // GET api/values
        [HttpGet]
        public string Get()
        
            //ViewModel is of type dynamic - just for testing
            dynamic x = new ExpandoObject();
            x.Test = "Yes";
            var viewWithViewModel = _viewRender.Render("eNotify/Confirm.cshtml", x);
            var viewWithoutViewModel = _viewRender.Render("MyFeature/Test.cshtml");
            return viewWithViewModel + viewWithoutViewModel;
        
    

【讨论】:

谢谢 - 与 ASP.NET Core 1.0.1 配合使用非常好。喜欢一般/服务方法:) 感谢@pholly,这也适用于 Core 1.1。它是一个非常好的解决问题的方法。有什么想法需要考虑 _ViewImports.cshtml、_ViewStart.cshtml 和 _Layout.cshtml,包括 _Layout.cshtml 中使用的 RenderBody 和 RenderSection 命令?我尝试将它们放在与正在加载的视图相同的文件夹中,但它们没有被使用,至少基于结果输出。 @TommyBaggett 我的代码基于此:github.com/aspnet/Entropy/blob/dev/samples/… 所以我不能相信 :) 我没有意识到 _Layout.cshtml 并且不会使用这样的 - 也许是 @987654336毕竟需要@来加载那些?试试我链接的服务,看看它是否有效。它使用ActionContext 谢谢@pholly,我试了一下,发现我的观点时遇到了问题。按照下面另一个答案中的建议,我尝试了 RazorLight 引擎 (github.com/toddams/RazorLight)。它工作得很好。 使用 ASP.NET Core 1.1.0 时,我遇到了一个问题,当我在这里开始使用该版本时该问题消失了:github.com/aspnet/Entropy/blob/dev/samples/…。在采用示例中的方法之前,我的错误的调用堆栈与此非常相似:github.com/aspnet/Mvc/issues/5505【参考方案3】:

过去,我在类库中使用RazorEngine,因为我的目标是在该类库中呈现模板。

据我了解,您似乎在 MVC 6.0 项目中,那么为什么不使用 RenderPartialViewToString() 方法而不必添加对 RazorEngine 的依赖项?

请记住,我只是因为好奇才问。

例如,在 VS2015 中,我创建了一个新的 ASP.NET Web 应用程序,并从 ASP.NET 5 Preview Templates 中选择了 Web Application 模板。

ViewModels 文件夹中,我创建了一个PersonViewModel

public class PersonViewModel

    public string FirstName  get; set; 
    public string LastName  get; set; 
    public string FullName
    
        get
        
            return string.Format("0 1", this.FirstName, this.LastName);
        
     

然后我创建了一个新的BaseController 并添加了一个RenderPartialViewToString() 方法:

public string RenderPartialViewToString(string viewName, object model)

    if (string.IsNullOrEmpty(viewName))
        viewName = ActionContext.ActionDescriptor.Name;

    ViewData.Model = model;

    using (StringWriter sw = new StringWriter())
    
        var engine = Resolver.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
        ViewEngineResult viewResult = engine.FindPartialView(ActionContext, viewName);

        ViewContext viewContext = new ViewContext(ActionContext, viewResult.View, ViewData, TempData, sw,new HtmlHelperOptions());

        var t = viewResult.View.RenderAsync(viewContext);
        t.Wait();

        return sw.GetStringBuilder().ToString();
    

感谢@DavidG 的method

Views--&gt;Shared 文件夹内,我创建了一个新的Templates 文件夹,我在其中添加了一个简单的RegistrationTemplate.cshtml View strongly typed 到我的PersonViewModel 喜欢所以:

@model MyWebProject.ViewModels.PersonViewModel
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Registration</title>
</head>
<body>
    <p>
        Hello, @Model.FullName
    </p>
</body>
</html>

最后一步是让我的Controller 继承自我的BaseController

public class MyController : BaseController

然后创建类似的东西:

public IActionResult Index()

    var model = new PersonViewModel();
    model.FirstName = "Frank";
    model.LastName = "Underwood";
    var emailbody = base.RenderPartialViewToString("Templates/RegistrationTemplate", model);

    return View();

当然,上面的例子没有用,因为我没有对变量emailbody 做任何事情,但我的想法是展示它是如何使用的。

此时,我可以(例如)调用EmailService 并传递emailbody

_emailService.SendEmailAsync("test@test.com", "registration", emailbody);

我不确定这是否适合您当前的任务。

【讨论】:

如何在类库中做同样的事情?你那里没有 ActionContext... 上面的例子展示了如何将视图渲染为字符串。该示例位于所有依赖项都已存在的 asp.net MVC 应用程序中。如果您打算在类库中使用类似的东西,则上述方法可能不是最好的方法,因为您不会拥有所有必要的依赖关系,什么都没有……为此,您可能想尝试一种不同的方法,例如这个:strathweb.com/2016/01/…【参考方案4】:

今天我已经完成了可以解决您问题的库。您可以在 ASP.NET 之外使用它,因为它不依赖它

例子:

string content = "Hello @Model.Name. Welcome to @Model.Title repository";

var model = new

  Name = "John Doe",
  Title = "RazorLight"
;

var engine = new RazorLightEngine();
string result = engine.ParseString(content, model);

//Output: Hello John Doe, Welcome to RazorLight repository

更多:https://github.com/toddams/RazorLight

【讨论】:

感谢您提供 RazorLight 库。只要您解析单个视图文件,这里提供的其他一些解决方案就可以工作。当我需要考虑支持 _ViewStart 和 _Layout 文件时,我切换到了您的库。您的库非常适合我的需求。 我很难让其他解决方案发挥作用。但这很好用! @toddams 你能帮我解决我打开的问题吗,我们正在尝试使用该库从动态模型问题https://github.com/toddams/RazorLight/issues/158生成动态模板 很好的解决方案。干杯。【参考方案5】:

为了改进@vlince 的答案(这对我来说不是开箱即用的),这就是我所做的:

1- 创建一个您的其他控制器将继承的基本控制器

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.IO;

namespace YourNameSpace

    public class BaseController : Controller
    
        protected ICompositeViewEngine viewEngine;

        public BaseController(ICompositeViewEngine viewEngine)
        
            this.viewEngine = viewEngine;
        

        protected string RenderViewAsString(object model, string viewName = null)
        
            viewName = viewName ?? ControllerContext.ActionDescriptor.ActionName;
            ViewData.Model = model;

            using (StringWriter sw = new StringWriter())
            
                IView view = viewEngine.FindView(ControllerContext, viewName, true).View;
                ViewContext viewContext = new ViewContext(ControllerContext, view, ViewData, TempData, sw, new HtmlHelperOptions());

                view.RenderAsync(viewContext).Wait();

                return sw.GetStringBuilder().ToString();
            
        
    

2- 继承基控制器并调用方法

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;

namespace YourNameSpace

    public class YourController : BaseController
    
        public YourController(ICompositeViewEngine viewEngine) : base(viewEngine)  

        public string Index(int? id)
        
            var model = new MyModel  Name = "My Name" ;

            return RenderViewAsString(model);
        
    

【讨论】:

谢谢 - 与 ASP.NET Core 1.0.1 配合使用非常好 TutorialController 实际上应该是示例中的 YourController 吗?否则,非常感谢这个想法。在使用 BaseForm 的旧 ASP.NET 表单时代,我或多或少已经忘记了这种技术 :)【参考方案6】:

ResolveUrlMethodName 已被删除。因此,在您的 CreateHost here 中,您试图设置一个不存在的属性:)。

我们决定将 ~/ 处理从核心 Razor 转移到在 Microsoft.AspNet.Mvc.Razor 程序集中实现的 TagHelper。这是删除该方法的位的commit。

希望这会有所帮助。

【讨论】:

感谢详细的解释!以后我会看的。【参考方案7】:

可以在此处找到仅使用 ASP.NET Core、没有外部库和反射的替代解决方案:https://weblogs.asp.net/ricardoperes/getting-html-for-a-viewresult-in-asp-net-core。 它只需要一个 ViewResult 和一个 HttpContext。

【讨论】:

【参考方案8】:

将部分视图转换为字符串响应的扩展方法。

public static class PartialViewToString

    public static async Task<string> ToString(this PartialViewResult partialView, ActionContext actionContext)
    
        using(var writer = new StringWriter())
        
            var services = actionContext.HttpContext.RequestServices;
            var executor = services.GetRequiredService<PartialViewResultExecutor>();
            var view = executor.FindView(actionContext, partialView).View;
            var viewContext = new ViewContext(actionContext, view, partialView.ViewData, partialView.TempData, writer, new HtmlHelperOptions());
            await view.RenderAsync(viewContext);
            return writer.ToString();
        
    

在您的控制器操作中使用。

public async Task<IActionResult> Index()

    return await PartialView().ToString(ControllerContext)

.NET 5 实现

public static async Task<string> ViewToString(this PartialViewResult partialView, Controller controller)
    
        using (var writer = new StringWriter())
        
            var services = controller.ControllerContext.HttpContext.RequestServices;
            var viewEngine = services.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
            var viewName = partialView.ViewName ?? controller.ControllerContext.ActionDescriptor.ActionName;
            var view = viewEngine.FindView(controller.ControllerContext, viewName, false).View;
            var viewContext = new ViewContext(controller.ControllerContext, view, partialView.ViewData, partialView.TempData, writer, new HtmlHelperOptions());
            await view.RenderAsync(viewContext);
            return writer.ToString();
        
    

【讨论】:

这似乎不再适用于 .NET 5。我不知道如何注册 PartialViewResultExecutor。 :(No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.PartialViewResultExecutor' has been registered.

以上是关于在 ASP.NET Core 中将 Razor 视图渲染为字符串的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET 5 中将 Razor 视图呈现为字符串

覆盖 asp.net core razor 页面中的 razor 视图

asp.net core中的Razor很慢

ASP.NET Core Razor 页面与完整 MVC Core [关闭]

如何在 ASP.Net Core Razor 页面上重定向

Asp.net core razor pages 加载部分页面