我想将Razor用作我在.NET Core中编写的.NET控制台应用程序中的模板引擎。
我遇到的独立Razor引擎(RazorEngine,RazorTemplates)都需要完整的.NET。我正在寻找一种适用于.NET Core的解决方案。
Answers:
最近,我创建了一个名为RazorLight的库。
它没有多余的依赖性,例如ASP.NET MVC部件,可以在控制台应用程序中使用。目前,它仅支持.NET Core(NetStandard1.6)-但这正是您所需要的。
这是一个简短的示例:
IRazorLightEngine engine = EngineFactory.CreatePhysical("Path-to-your-views");
// Files and strong models
string resultFromFile = engine.Parse("Test.cshtml", new Model("SomeData"));
// Strings and anonymous models
string stringResult = engine.ParseString("Hello @Model.Name", new { Name = "John" });
dotnet publish
。
这是一个示例代码,仅取决于Razor(用于解析和C#代码生成)和Roslyn(用于C#代码编译,但您也可以使用旧的CodeDom)。
这段代码中没有MVC,因此,没有View,没有.cshtml文件,没有Controller,只有Razor源代码解析和编译的运行时执行。尽管仍然存在模型的概念。
您只需要添加以下nuget软件包:(已Microsoft.AspNetCore.Razor.Language
通过v3.1.7测试),Microsoft.AspNetCore.Razor.Runtime
(已通过v2.2.0测试)和Microsoft.CodeAnalysis.CSharp
(已通过v3.7.0测试)nuget。
此C#源代码与NETCore 3.1(对于较旧的版本,请查看此答案的历史记录),NETStandard 2和.NET Framework兼容。要对其进行测试,只需创建一个.NET框架或.NET核心控制台应用程序,将其粘贴,添加nuget并手动创建hello.txt文件即可。
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace RazorTemplate
{
class Program
{
static void Main(string[] args)
{
// points to the local path
var fs = RazorProjectFileSystem.Create(".");
// customize the default engine a little bit
var engine = RazorProjectEngine.Create(RazorConfiguration.Default, fs, (builder) =>
{
// InheritsDirective.Register(builder); // in .NET core 3.1, compatibility has been broken (again), and this is not needed anymore...
builder.SetNamespace("MyNamespace"); // define a namespace for the Template class
});
// get a razor-templated file. My "hello.txt" template file is defined like this:
//
// @inherits RazorTemplate.MyTemplate
// Hello @Model.Name, welcome to Razor World!
//
var item = fs.GetItem("hello.txt", null);
// parse and generate C# code
var codeDocument = engine.Process(item);
var cs = codeDocument.GetCSharpDocument();
// outputs it on the console
//Console.WriteLine(cs.GeneratedCode);
// now, use roslyn, parse the C# code
var tree = CSharpSyntaxTree.ParseText(cs.GeneratedCode);
// define the dll
const string dllName = "hello";
var compilation = CSharpCompilation.Create(dllName, new[] { tree },
new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location), // include corlib
MetadataReference.CreateFromFile(typeof(RazorCompiledItemAttribute).Assembly.Location), // include Microsoft.AspNetCore.Razor.Runtime
MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location), // this file (that contains the MyTemplate base class)
// for some reason on .NET core, I need to add this... this is not needed with .NET framework
MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Runtime.dll")),
// as found out by @Isantipov, for some other reason on .NET Core for Mac and Linux, we need to add this... this is not needed with .NET framework
MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "netstandard.dll"))
},
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); // we want a dll
// compile the dll
string path = Path.Combine(Path.GetFullPath("."), dllName + ".dll");
var result = compilation.Emit(path);
if (!result.Success)
{
Console.WriteLine(string.Join(Environment.NewLine, result.Diagnostics));
return;
}
// load the built dll
Console.WriteLine(path);
var asm = Assembly.LoadFile(path);
// the generated type is defined in our custom namespace, as we asked. "Template" is the type name that razor uses by default.
var template = (MyTemplate)Activator.CreateInstance(asm.GetType("MyNamespace.Template"));
// run the code.
// should display "Hello Killroy, welcome to Razor World!"
template.ExecuteAsync().Wait();
}
}
// the model class. this is 100% specific to your context
public class MyModel
{
// this will map to @Model.Name
public string Name => "Killroy";
}
// the sample base template class. It's not mandatory but I think it's much easier.
public abstract class MyTemplate
{
// this will map to @Model (property name)
public MyModel Model => new MyModel();
public void WriteLiteral(string literal)
{
// replace that by a text writer for example
Console.Write(literal);
}
public void Write(object obj)
{
// replace that by a text writer for example
Console.Write(obj);
}
public async virtual Task ExecuteAsync()
{
await Task.Yield(); // whatever, we just need something that compiles...
}
}
}
MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"netstandard.dll")),
对于2020年或之后的所有人:我已经开始https://github.com/adoconnection/RazorEngineCore
它具有最新的ASP.NET Core 3.1.1 Razor及其语法功能。
用法与RazorEngine完全相同:
RazorEngine razorEngine = new RazorEngine();
RazorEngineCompiledTemplate template = razorEngine.Compile("Hello @Model.Name");
string result = template.Run(new
{
Name = "Alex"
});
Console.WriteLine(result);
快速保存和加载
// save to file
template.SaveToFile("myTemplate.dll");
//save to stream
MemoryStream memoryStream = new MemoryStream();
template.SaveToStream(memoryStream);
var template1 = RazorEngineCompiledTemplate.LoadFromFile("myTemplate.dll");
var template2 = RazorEngineCompiledTemplate.LoadFromStream(myStream);
System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.CSharp' or one of its dependencies. The system cannot find the file specified.
此工作了.NET框架的任何机会呢?
PreserveCompilationContext
在项目设置中是否也需要?如果可能的话,我宁愿避免这种情况。
.aspnet / Entropy / samples / Mvc.RenderViewToString中有一个.NET Core 1.0的工作示例。由于这可能会改变或消失,因此我将在这里详细说明我在自己的应用程序中使用的方法。
Tl; dr -Razor在MVC之外效果很好!尽管我将在下面演示一个简单的示例,但是该方法可以处理更复杂的渲染方案,例如局部视图,以及将对象注入视图。
核心服务如下所示:
RazorViewToStringRenderer.cs
using System;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
namespace RenderRazorToString
{
public class RazorViewToStringRenderer
{
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public RazorViewToStringRenderer(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderViewToString<TModel>(string name, TModel model)
{
var actionContext = GetActionContext();
var viewEngineResult = _viewEngine.FindView(actionContext, name, false);
if (!viewEngineResult.Success)
{
throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", name));
}
var view = viewEngineResult.View;
using (var output = new StringWriter())
{
var viewContext = new ViewContext(
actionContext,
view,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return output.ToString();
}
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
}
一个简单的测试控制台应用程序只需初始化服务(和一些支持服务),然后调用它:
Program.cs
using System;
using System.Diagnostics;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.PlatformAbstractions;
namespace RenderRazorToString
{
public class Program
{
public static void Main()
{
// Initialize the necessary services
var services = new ServiceCollection();
ConfigureDefaultServices(services);
var provider = services.BuildServiceProvider();
var renderer = provider.GetRequiredService<RazorViewToStringRenderer>();
// Build a model and render a view
var model = new EmailViewModel
{
UserName = "User",
SenderName = "Sender"
};
var emailContent = renderer.RenderViewToString("EmailTemplate", model).GetAwaiter().GetResult();
Console.WriteLine(emailContent);
Console.ReadLine();
}
private static void ConfigureDefaultServices(IServiceCollection services)
{
var applicationEnvironment = PlatformServices.Default.Application;
services.AddSingleton(applicationEnvironment);
var appDirectory = Directory.GetCurrentDirectory();
var environment = new HostingEnvironment
{
WebRootFileProvider = new PhysicalFileProvider(appDirectory),
ApplicationName = "RenderRazorToString"
};
services.AddSingleton<IHostingEnvironment>(environment);
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Clear();
options.FileProviders.Add(new PhysicalFileProvider(appDirectory));
});
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddLogging();
services.AddMvc();
services.AddSingleton<RazorViewToStringRenderer>();
}
}
}
假设您有一个视图模型类:
EmailViewModel.cs
namespace RenderRazorToString
{
public class EmailViewModel
{
public string UserName { get; set; }
public string SenderName { get; set; }
}
}
以及布局和查看文件:
视图/_Layout.cshtml
<!DOCTYPE html>
<html>
<body>
<div>
@RenderBody()
</div>
<footer>
Thanks,<br />
@Model.SenderName
</footer>
</body>
</html>
视图/EmailTemplate.cshtml
@model RenderRazorToString.EmailViewModel
@{
Layout = "_EmailLayout";
}
Hello @Model.UserName,
<p>
This is a generic email about something.<br />
<br />
</p>
The type 'Attribute' is defined in an assembly that is not referenced. You must add a reference to assembly 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'
-我不确定如何告诉razor加载它需要的所有依赖项-有什么想法吗?
The type 'Attribute' is defined in an assembly that is not referenced
错误的人,添加即可<PreserveCompilationContext>true</PreserveCompilationContext>
解决问题。
IWebHostEnvironment
DI容器中添加一个实例,并使用MvcRazorRuntimeCompilationOptions
该类提供文件提供程序而不是RazorViewEngineOptions
该类。RazorViewToStringRenderer
此处提供了一个详细的实例化示例:corstianboerman.com/2019-12-25/…。
这是一个使Nate的答案在ASP.NET Core 2.0项目中用作范围服务的类。
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
namespace YourNamespace.Services
{
public class ViewRender : IViewRender
{
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public ViewRender(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderAsync(string name)
{
return await RenderAsync<object>(name, null);
}
public async Task<string> RenderAsync<TModel>(string name, TModel model)
{
var actionContext = GetActionContext();
var viewEngineResult = _viewEngine.FindView(actionContext, name, false);
if (!viewEngineResult.Success)
{
throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", name));
}
var view = viewEngineResult.View;
using (var output = new StringWriter())
{
var viewContext = new ViewContext(
actionContext,
view,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return output.ToString();
}
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext {RequestServices = _serviceProvider};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
public interface IViewRender
{
Task<string> RenderAsync(string name);
Task<string> RenderAsync<TModel>(string name, TModel model);
}
}
在Startup.cs中
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IViewRender, ViewRender>();
}
在控制器中
public class VenuesController : Controller
{
private readonly IViewRender _viewRender;
public VenuesController(IViewRender viewRender)
{
_viewRender = viewRender;
}
public async Task<IActionResult> Edit()
{
string html = await _viewRender.RenderAsync("Emails/VenuePublished", venue.Name);
return Ok();
}
}
我花了几天时间剃光剃须刀,但是它有很多缺陷,例如没有html helper(@ Html。*)或url helper,以及其他一些怪癖。
这是一个封装在mvc应用程序外部使用的解决方案。它确实需要对aspnet core和mvc的程序包引用,但是它们很容易添加到服务或控制台应用程序中。无需控制器或Web服务器。RenderToStringAsync是用于将视图呈现为字符串的调用方法。
优点是您可以像在.net核心Web项目中一样编写视图。您可以使用相同的@Html和其他辅助函数和方法。
您可以使用自己的自定义提供程序在razor视图选项设置中替换或添加到物理文件提供程序,以从数据库,Web服务调用等加载视图。在Windows和Linux上使用.net core 2.2测试。
请注意,您的.csproj文件必须将其作为第一行:
<Project Sdk="Microsoft.NET.Sdk.Web">
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
namespace RazorRendererNamespace
{
/// <summary>
/// Renders razor pages with the absolute minimum setup of MVC, easy to use in console application, does not require any other classes or setup.
/// </summary>
public class RazorRenderer : ILoggerFactory, ILogger
{
private class ViewRenderService : IDisposable, ITempDataProvider, IServiceProvider
{
private static readonly System.Net.IPAddress localIPAddress = System.Net.IPAddress.Parse("127.0.0.1");
private readonly Dictionary<string, object> tempData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
public ViewRenderService(IRazorViewEngine viewEngine,
IHttpContextAccessor httpContextAccessor,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_httpContextAccessor = httpContextAccessor;
_tempDataProvider = tempDataProvider ?? this;
_serviceProvider = serviceProvider ?? this;
}
public void Dispose()
{
}
public async Task<string> RenderToStringAsync<TModel>(string viewName, TModel model, ExpandoObject viewBag = null, bool isMainPage = false)
{
HttpContext httpContext;
if (_httpContextAccessor?.HttpContext != null)
{
httpContext = _httpContextAccessor.HttpContext;
}
else
{
DefaultHttpContext defaultContext = new DefaultHttpContext { RequestServices = _serviceProvider };
defaultContext.Connection.RemoteIpAddress = localIPAddress;
httpContext = defaultContext;
}
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using (var sw = new StringWriter())
{
var viewResult = _viewEngine.FindView(actionContext, viewName, isMainPage);
if (viewResult.View == null)
{
viewResult = _viewEngine.GetView("~/", viewName, isMainPage);
}
if (viewResult.View == null)
{
return null;
}
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
};
if (viewBag != null)
{
foreach (KeyValuePair<string, object> kv in (viewBag as IDictionary<string, object>))
{
viewDictionary.Add(kv.Key, kv.Value);
}
}
var viewContext = new ViewContext(
actionContext,
viewResult.View,
viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return sw.ToString();
}
}
object IServiceProvider.GetService(Type serviceType)
{
return null;
}
IDictionary<string, object> ITempDataProvider.LoadTempData(HttpContext context)
{
return tempData;
}
void ITempDataProvider.SaveTempData(HttpContext context, IDictionary<string, object> values)
{
}
}
private readonly string rootPath;
private readonly ServiceCollection services;
private readonly ServiceProvider serviceProvider;
private readonly ViewRenderService viewRenderer;
public RazorRenderer(string rootPath)
{
this.rootPath = rootPath;
services = new ServiceCollection();
ConfigureDefaultServices(services);
serviceProvider = services.BuildServiceProvider();
viewRenderer = new ViewRenderService(serviceProvider.GetRequiredService<IRazorViewEngine>(), null, null, serviceProvider);
}
private void ConfigureDefaultServices(IServiceCollection services)
{
var environment = new HostingEnvironment
{
WebRootFileProvider = new PhysicalFileProvider(rootPath),
ApplicationName = typeof(RazorRenderer).Assembly.GetName().Name,
ContentRootPath = rootPath,
WebRootPath = rootPath,
EnvironmentName = "DEVELOPMENT",
ContentRootFileProvider = new PhysicalFileProvider(rootPath)
};
services.AddSingleton<IHostingEnvironment>(environment);
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Clear();
options.FileProviders.Add(new PhysicalFileProvider(rootPath));
});
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddSingleton<ILoggerFactory>(this);
var diagnosticSource = new DiagnosticListener(environment.ApplicationName);
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddMvc();
}
public void Dispose()
{
}
public Task<string> RenderToStringAsync<TModel>(string viewName, TModel model, ExpandoObject viewBag = null, bool isMainPage = false)
{
return viewRenderer.RenderToStringAsync(viewName, model, viewBag, isMainPage);
}
void ILoggerFactory.AddProvider(ILoggerProvider provider)
{
}
IDisposable ILogger.BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
ILogger ILoggerFactory.CreateLogger(string categoryName)
{
return this;
}
bool ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
{
return false;
}
void ILogger.Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
}
Microsoft.NET.Sdk.Web
表示该项目需要ASP.NET Core运行时才能运行。对于控制台/桌面应用程序,这可能非常繁琐且不必要。