Usando Razor fuera de MVC en .NET Core

Me gustaría usar Razor como motor de plantillas en una aplicación de consola .NET que estoy escribiendo en .NET Core.

Todos los motores Razor independientes que he encontrado (RazorEngine, RazorTemplates) requieren .NET completo. Estoy buscando una solución que funcione con .NET Core.

Recientemente he creado una biblioteca llamada RazorLight .

No tiene dependencias redundantes, como las partes ASP.NET MVC y se puede usar en aplicaciones de consola. Por ahora solo es compatible con .NET Core (NetStandard1.6), pero eso es exactamente lo que necesita.

Aquí hay un breve ejemplo:

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" }); 

Hay un ejemplo de trabajo para .NET Core 1.0 en aspnet / Entropy / samples / Mvc.RenderViewToString . Como esto podría cambiar o desaparecer, detallaré el enfoque que estoy usando en mis propias aplicaciones aquí.

Tl; dr – Razor funciona realmente bien fuera de MVC! Este enfoque puede manejar escenarios de representación más complejos, como vistas parciales e inyectar objetos en las vistas también, aunque solo mostraré un ejemplo simple a continuación.


El servicio central se ve así:

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 RenderViewToString(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( 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()); } } } 

Una aplicación de consola de prueba simple solo necesita inicializar el servicio (y algunos servicios de soporte) y llamarlo:

Progtwig.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(); // 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(environment); services.Configure(options => { options.FileProviders.Clear(); options.FileProviders.Add(new PhysicalFileProvider(appDirectory)); }); services.AddSingleton(); var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); services.AddSingleton(diagnosticSource); services.AddLogging(); services.AddMvc(); services.AddSingleton(); } } } 

Esto supone que tienes una clase de modelo de vista:

EmailViewModel.cs

 namespace RenderRazorToString { public class EmailViewModel { public string UserName { get; set; } public string SenderName { get; set; } } } 

Y archivos de diseño y visualización:

Vistas / _Layout.cshtml

    
@RenderBody()
Thanks,
@Model.SenderName

Vistas / EmailTemplate.cshtml

 @model RenderRazorToString.EmailViewModel @{ Layout = "_EmailLayout"; } Hello @Model.UserName, 

This is a generic email about something.

Aquí hay un código de muestra que solo depende de Razor (para el análisis y la generación de código C #) y Roslyn (para la comstackción de código C #, pero también puede usar el CodeDom antiguo).

No hay MVC en ese fragmento de código, por lo tanto, no hay Vista, no hay archivos .cshtml, no hay Controlador, solo análisis de fuente Razor y ejecución en tiempo de ejecución comstackda. Sin embargo, todavía existe la noción de modelo.

Solo deberá agregar los siguientes paquetes de nuget: Microsoft.AspNetCore.Razor.Language (v2.1.1), Microsoft.AspNetCore.Razor.Runtime (v2.1.1) y Microsoft.CodeAnalysis.CSharp (v2.8.2) nugets.

Este código fuente de C # es compatible con NETCore, NETStandard 2 y .NET Framework. Para probarlo, simplemente cree una aplicación de consola central .NET o .NET, péguela y agregue los nugets.

 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); 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"); // parse and generate C# code, outputs it on the console //var cs = te.GenerateCode(item); //Console.WriteLine(cs.GeneratedCode); var codeDocument = engine.Process(item); var cs = codeDocument.GetCSharpDocument(); // now, use roslyn, parse the C# code var tree = CSharpSyntaxTree.ParseText(cs.GeneratedCode); // define the dll const string dllName = "hello"; var comstacktion = CSharpComstacktion.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 CSharpComstacktionOptions(OutputKind.DynamicallyLinkedLibrary)); // we want a dll // compile the dll string path = Path.Combine(Path.GetFullPath("."), dllName + ".dll"); var result = comstacktion.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... } } } 

Aquí hay una clase para que la respuesta de Nate funcione como un servicio de ámbito en un proyecto 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 RenderAsync(string name) { return await RenderAsync(name, null); } public async Task RenderAsync(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( 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 RenderAsync(string name); Task RenderAsync(string name, TModel model); } } 

En Startup.cs

 public void ConfigureServices(IServiceCollection services) { services.AddScoped(); } 

En un controlador

 public class VenuesController : Controller { private readonly IViewRender _viewRender; public VenuesController(IViewRender viewRender) { _viewRender = viewRender; } public async Task Edit() { string html = await _viewRender.RenderAsync("Emails/VenuePublished", venue.Name); return Ok(); } }