I would like to use Razor as a templating engine in a .NET console application that I'm writing in .NET Core.
The standalone Razor engines I've come across (RazorEngine, RazorTemplates) all require full .NET. I'm looking for a solution that works with .NET Core.
Here is a sample code that only depends on Razor (for parsing and C# code generation) and Roslyn (for C# code compilation, but you could use the old CodeDom as well).
There is no MVC in that piece of code, so, no View, no .cshtml files, no Controller, just Razor source parsing and compiled runtime execution. There is still the notion of Model though.
You will only need to add following nuget packages: Microsoft.AspNetCore.Razor.Language (tested with v5.0.5), Microsoft.AspNetCore.Razor.Runtime (tested with v2.2.0) and Microsoft.CodeAnalysis.CSharp (tested with v3.9.0) nugets.
This C# source code is compatible with .NET 5, NETCore 3.1 (for older versions check this answer's history), NETStandard 2 and .NET Framework. To test it just create a .NET framework or .NET core console app, paste it, add the nugets, and create the hello.txt file by hand (it must be located aside the executables).
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; // needed or not depends on .NET version
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...
}
}
}
Recently I've created a library called RazorLight.
It has no redundant dependencies, like ASP.NET MVC parts and can be used in console applications. For now it only supports .NET Core (NetStandard1.6) - but that's exactly what you need.
Here is a short example:
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" });
For anyone in 2021+ here:
I've started https://github.com/adoconnection/RazorEngineCore
It has latest ASP.NET Core 5 Razor and it's syntax features.
Usage is quite the same as RazorEngine:
RazorEngine razorEngine = new RazorEngine();
RazorEngineCompiledTemplate template = razorEngine.Compile("Hello #Model.Name");
string result = template.Run(new
{
Name = "Alex"
});
Console.WriteLine(result);
Fast saving and loading
// 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);
There's a working example for .NET Core 1.0 at aspnet/Entropy/samples/Mvc.RenderViewToString. Since this might change or go away, I'll detail the approach I'm using in my own applications here.
Tl;dr - Razor works really well outside of MVC! This approach can handle more complex rendering scenarios like partial views and injecting objects into views as well, although I'll just demonstrate a simple example below.
The core service looks like this:
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());
}
}
}
A simple test console app just needs to initialize the service (and some supporting services), and call it:
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>();
}
}
}
This assumes that you have a view model class:
EmailViewModel.cs
namespace RenderRazorToString
{
public class EmailViewModel
{
public string UserName { get; set; }
public string SenderName { get; set; }
}
}
And layout and view files:
Views/_Layout.cshtml
<!DOCTYPE html>
<html>
<body>
<div>
#RenderBody()
</div>
<footer>
Thanks,<br />
#Model.SenderName
</footer>
</body>
</html>
Views/EmailTemplate.cshtml
#model RenderRazorToString.EmailViewModel
#{
Layout = "_EmailLayout";
}
Hello #Model.UserName,
<p>
This is a generic email about something.<br />
<br />
</p>
Here is a class to get Nate's answer working as a scoped service in an ASP.NET Core 2.0 project.
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);
}
}
In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IViewRender, ViewRender>();
}
In a controller
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();
}
}
If you are in 2022, there's an easy to use library called Razor.Templating.Core.
It works out of the box for MVC, API, Console and many other types of applications.
Supports .NET Core 3.1, .NET 5, .NET 6
Supports most of Razor features like ViewModel, ViewBag, ViewData, TagHelpers, Partial Views, ViewComponents and more
Supports Single File Publish, ReadyToRun
Usage is much simpler:
var htmlString = await RazorTemplateEngine.RenderAsync("/Views/ExampleView.cshtml", model, viewData);
Refer documentation here
P.S: I'm the author of this library.
I spent several days fiddling with razor light, but it has a number of deficiencies such as not having html helpers (#Html.*) or url helpers, and other quirks.
Here is a solution that is encapsulated for usage outside of an mvc app. It does require package references to aspnet core and mvc, but those are easy to add to a service or console application. No controllers or web server are needed. RenderToStringAsync is the method to call to render a view to a string.
The advantage is that you can write your views the same way you would in a .net core web project. You can use the same #Html and other helper functions and methods.
You can replace or add to the physical file provider in the razor view options setup with your own custom provider to load views from database, web service call, etc. Tested with .net core 2.2 on Windows and Linux.
Please note that your .csproj file must have this as the top line:
<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)
{
}
}
}
Related
I have a Azure Function with 2 triggers:
I’m registering IService in my Startup like so:
I need a different configuration in the Service class depending on which trigger that is calling DoWork()? How can I achieve this using DI?
public class Service : IService
{
public Service(/*Configuration to be injected depends on calling trigger */)
{ }
public void DoWork()
{ }
}
Configuration extract:
Thankyou user1672994. Posting your suggestion as an answer so that it will be helpful for other community members who face similar kind of issues.
Below is the example code to implement todo work items where this will be helpful in resolving your issue.
using AZV3CleanArchitecture.Models;
using AZV3CleanArchitecture.Options;
using AZV3CleanArchitecture.Providers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace AZV3CleanArchitecture.Services
{
public class ToDoItemsService : IToDoItemsService
{
private readonly HttpClient httpClient;
private readonly ToDoItemsServiceOptions toDoItemsServiceOptions;
private readonly ILogger<ToDoItemsService> logger;
public ToDoItemsService(HttpClient httpClient, IOptions<ToDoItemsServiceOptions> toDoItemsServiceOptions, ILogger<ToDoItemsService> logger)
{
this.httpClient = httpClient;
this.toDoItemsServiceOptions = toDoItemsServiceOptions.Value;
this.logger = logger;
}
public async Task<ToDoItem> GetToDoItem(int id)
{
logger.LogInformation($"Retrieving item: {{{Constants.TodoItemId}}}", id);
var getUrl = $"{this.toDoItemsServiceOptions.BaseUrl.TrimEnd('/')}/todos/{id}";
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, getUrl))
{
using (var response = await this.httpClient.SendAsync(requestMessage))
{
string responseString = await response.Content.ReadAsStringAsync();
logger.LogWarning($"Retrieved item: {{{Constants.TodoItemId}}}. Logged as warning for demo.", id);
return JsonConvert.DeserializeObject<ToDoItem>(responseString);
}
}
}
public async Task<IEnumerable<ToDoItem>> GetAllToDoItems(int id)
{
logger.LogInformation($"Retrieving all todo items");
var getUrl = $"{this.toDoItemsServiceOptions.BaseUrl.TrimEnd('/')}/todos";
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, getUrl))
{
using (var response = await this.httpClient.SendAsync(requestMessage))
{
string responseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<IEnumerable<ToDoItem>>(responseString);
}
}
}
public async Task<ToDoItem> CreateToDoItem(ToDoItem toDoItem)
{
// call service and return the output
return await Task.FromResult(new ToDoItem() { Id = 1, UserId = 1, Title = "Some Dummy Title", Completed = true });
}
public Task<ToDoItem> UpdateToDoItem(ToDoItem toDoItem)
{
throw new System.NotImplementedException();
}
}
}
for further information check the ToDoItemServices link.
Basically User enters page, generates report and decides whether it should be saved or not.
If yes, then I have to save this html as static html file.
It'd be really nice if that was something like:
public IActionResult GetReport()
{
(...)
string html = View(model).ToString();
save_to_database(html);
return View(model);
}
But, the only solution that I managed to find is putting button on that page
which executes JavaScript like this one:
var html = new XMLSerializer().serializeToString(document);
sendHTMLViaAPI(html);
and sending that html to API via post.
So, I'm curious whether there are C#-ish ways to do it? in Controller's method.
Also: Is that potentially dangerous? E.g. session things can be saved in that html file or user can send content of non-HTML file via API...
If I understand you meant correctly, this solution may be what you're looking for:
[HttpPost]
public IActionResult GetHTML()
{
var model = new ModelClass() { Content = "Hi!" };
// or
// return PartialView("GetHTML", model);
return PartialView(nameof(GetHTML), model);
}
In the file GetHTML.cshtml:
#model ModelClass
<div>
Content: #Model.Content
</div>
In the clientside, when user wants to get the HTML as string, you can try to use jquery to get it:
$.post('/home/gethtml').done(function (html) {
// html is a string here..
// <div>
// Content: Hi!
// </div>
$('body').append(html);
});
Or creating your custom service:
ViewRender.cs
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;
using System;
using System.IO;
public class ViewRender : IViewRender
{
private IRazorViewEngine _viewEngine;
private ITempDataProvider _tempDataProvider;
private IServiceProvider _serviceProvider;
public ViewRender(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public string Render(string name)
{
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<string>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = null
},
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
output,
new HtmlHelperOptions());
view.RenderAsync(viewContext).GetAwaiter().GetResult();
return output.ToString();
}
}
public string Render<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());
view.RenderAsync(viewContext).GetAwaiter().GetResult();
return output.ToString();
}
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = _serviceProvider;
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
IViewRender.cs:
public interface IViewRender
{
string Render(string name);
string Render<TModel>(string name, TModel model);
}
Startup.cs:
services.AddTransient<IViewRender, ViewRender>();
Usage:
public class HomeController : Controller
{
private readonly IViewRender _viewRender { get; set; }
public HomeController(IViewRender viewRender)
{
_viewRender = viewRender;
}
public IActionResult GetHTML()
{
string htmlWithoutModel = _viewRender.Render("Home/GetHTML");
var model = new ModelClass() { Content = "Hi!" };
string htmlWithModel = _viewRender.Render<ModelClass>("Home/GetHTML", model);
//...
}
}
Return View as String in .NET Core
Copy of response from that url^
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System.IO;
using System.Threading.Tasks;
namespace CC.Web.Helpers
{
public static class ControllerExtensions
{
public static async Task<string> RenderViewAsync<TModel>(this Controller controller, string viewName, TModel model, bool partial = false)
{
if (string.IsNullOrEmpty(viewName))
{
viewName = controller.ControllerContext.ActionDescriptor.ActionName;
}
controller.ViewData.Model = model;
using (var writer = new StringWriter())
{
IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
ViewEngineResult viewResult = viewEngine.FindView(controller.ControllerContext, viewName, !partial);
if (viewResult.Success == false)
{
return $"A view with the name {viewName} could not be found";
}
ViewContext viewContext = new ViewContext(
controller.ControllerContext,
viewResult.View,
controller.ViewData,
controller.TempData,
writer,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return writer.GetStringBuilder().ToString();
}
}
}
}
Then just implement with:
viewHtml = await this.RenderViewAsync("Report", model);
Or this for a PartialView:
partialViewHtml = await this.RenderViewAsync("Report", model, true);
I hava a problem with unit testing asp.net core mvc controller!
the problem is that in my controller, i use sessions:
[HttpPost]
public IActionResult OpretLobby(LobbyViewModel lobby)
{
try
{
//find brugeren der har lavet lobby
var currentUser = HttpContext.Session.GetObjectFromJson<User>("user");
//save as a lobby
ILobby nyLobby = new Lobby(currentUser.Username)
{
Id = lobby.Id
};
SessionExtension.SetObjectAsJson(HttpContext.Session, lobby.Id, nyLobby);
//add to the list
_lobbyList.Add(nyLobby);
return RedirectToAction("Lobby","Lobby",lobby);
}
this all works perfectly well online on the server, nothing wrong here.
BUT when it comes to the demand of unit testing this whole thing, its not so perfect anymore.
basicly the problem is, that i cant get access to my session from a test.. i have tryed in many ways to create mocks and what not, but most of the solutions work for .net framework, and not for .net core for some reason! please help im in pain!
note:
i used a dummy version of a test to isolate this problem:
[Test]
public void TestIsWorking()
{
SessionExtension.SetObjectAsJson(uut.HttpContext.Session, "user", _savedUser);
//ViewResult result = uut.OpretLobby(lobbyViewModel) as ViewResult;
//Assert.AreEqual("OpretLobby", result.ViewName);
Assert.AreEqual(1,1);
}
goes wrong also here trying to set the session for a user :/
It seems that GetObjectFromJson is an extension method. If so, we could not mock static method easily.
I normally create an abstraction for that kind of scenario. Then register it in DI container, and inject the dependency to the controller.
Startup.cs
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<IUserSession, UserSession>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
...
}
...
}
Abstraction
public class User
{
public string Username { get; set; }
}
public interface IUserSession
{
User User { get; }
}
public class UserSession : IUserSession
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserSession(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public User User => _httpContextAccessor.HttpContext.Session.GetObjectFromJson<User>("user");
}
public static class SessionExtensions
{
public static User GetObjectFromJson<User>(
this ISession sesson, string json) where User : new()
{
return new User(); // Dummy extension method just to test OP's code
}
}
Controller
public class HomeController : Controller
{
private readonly IUserSession _userSession;
public HomeController(IUserSession userSession)
{
_userSession = userSession;
}
public IActionResult OpretLobby()
{
var currentUser = _userSession.User;
return View(currentUser);
}
}
}
Unit Tests
using AspNetCore.Controllers;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;
namespace XUnitTestProject1
{
public class HomeControllerTests
{
private readonly User _user;
public HomeControllerTests()
{
_user = new User {Username = "johndoe"};
}
[Fact]
public void OpretLobby_Test()
{
// Arrange
var mockUserSession = new Mock<IUserSession>();
mockUserSession.Setup(x => x.User).Returns(_user);
var sut = new HomeController(mockUserSession.Object);
// Act
var result = sut.OpretLobby();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var user = Assert.IsType<User>(viewResult.Model);
Assert.Equal(_user.Username, user.Username);
}
}
}
#nicholas-ladefoged
I would rather recommend you use integration testing when you need to get some content from session
asp.net core has an awesome TestHost nuget package which can help you validate logic using integration way for testing.
Try add a below code snippet
TestServer server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureTestServices(services =>
{
services.AddHttpContextAccessor();
}));
var client = server.CreateClient();
var response = await client.GetAsync(""); // I'm using GetAsync just for sample
here you go! You will have a real session that you can test
BTW, Moq library hasn't ability to mock a static methods, so there a lot of issues. I think that will be more easier to use an integration test in your situation
Win solved it! Here is the final test code if anyone is wondering.
// Arrange
var mockUserSession = new Mock<IUserSession>();
mockUserSession.Setup(x => x.User).Returns(_savedUser);
var sut = new LobbyController(FakeSwagCommunication,mockUserSession.Object);
// Act
var result = sut.OpretLobby(_lobbyViewModel);
// Assert
Assert.IsInstanceOf<RedirectToActionResult>(result);
For my project I currently implemented a ViewRender for my asp.net core application. It generates Views without a controller to html, this works fine using the following code:
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderToStringAsync(string viewName, object model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
string viewgerendered = "";
try
{
using (var sw = new StringWriter())
{
var viewResult = _razorViewEngine.GetView(viewName, viewName, false);
if (viewResult.View == null)
{
throw new ArgumentNullException($"{viewName} does not match any available view");
}
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
};
var viewContext = new ViewContext(
actionContext,
viewResult.View,
viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
viewgerendered = sw.ToString();
return viewgerendered;
}
}
catch (Exception e)
{
object temp = e.Message + " - " + e.StackTrace;
return temp.ToString();
}
}
public Task RenderToStringAsync(string v)
{
throw new NotImplementedException();
}
}
Source: https://ppolyzos.com/2016/09/09/asp-net-core-render-view-to-string/
Changes which are made to views which use this renderer are not updated without restarting the application itself. Diving further into it, the views are cached. A comment within the source mentions using the _razorViewEngine.GetView method should get rid of my caching issue. However this doesn't work.
What I got, trying to figure out a way to register a new ViewRender, with a slight modification of the ViewRenderService.
//Seems not to be available on asp.net core 2.0...
services.AddMvc().Configure<MvcViewOptions>(options =>
{
options.ViewEngines.Clear();
options.ViewEngines.Add(typeof(CustomViewEngine));
});
And to overload the RazorViewEngine to expose the ViewLookupCache, where supposedly the view cache is located.
public class CustomViewEngine : RazorViewEngine
{
public CustomViewEngine(
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
IOptions<RazorViewEngineOptions> optionsAccessor,
Microsoft.AspNetCore.Razor.Language.RazorProject razorProject,
ILoggerFactory loggerFactory,
System.Diagnostics.DiagnosticSource diagnosticSource) :
base(pageFactory, pageActivator, htmlEncoder, optionsAccessor,razorProject,loggerFactory, diagnosticSource){ }
public void RemoveCachedView(string view)
{
this.ViewLookupCache.Remove(view);
}
}
There's not a lot to find on how caching is done within asp.net core 2.0 for views and clearing a particular view / set of. Basically I want to find a way how I can flush an entire selection of cached views as a command, for performance reasons.
Edit 13-04-2018
As suggested by K Finley, I tried emptying the ViewLookupCache as suggested. The code in short;
In my startup.cs ConfigureServices (not entirely sure if this is how a custom viewengine is registered).
services.AddSingleton<IRazorViewEngine, CustomViewEngine>();
services.AddSingleton<IViewRenderService, ViewRenderService>();
The custom view engine:
public class CustomViewEngine : RazorViewEngine
{
public CustomViewEngine(
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
IOptions<RazorViewEngineOptions> optionsAccessor,
Microsoft.AspNetCore.Razor.Language.RazorProject razorProject,
ILoggerFactory loggerFactory,
System.Diagnostics.DiagnosticSource diagnosticSource) :
base(pageFactory, pageActivator, htmlEncoder, optionsAccessor, razorProject, loggerFactory, diagnosticSource)
{ }
public void RemoveViewFromCache(string viewName, string controller, bool isLayout, bool isPartial = false, string pageName = null, string areaName = null)
{
var key = new ViewLocationCacheKey(viewName, controller, areaName, pageName, !isLayout | !isPartial, isLayout ? null : new Dictionary<string, string>(StringComparer.Ordinal));
base.ViewLookupCache.Remove(key);
}
public void RemoveViewFromCache(string viewName, bool isLayout)
{
//Code uses this one
var key = new ViewLocationCacheKey(viewName, isLayout);
base.ViewLookupCache.Remove(key);
}
}
And modified the original ViewRenderService...
public class ViewRenderService : IViewRenderService
{
private CustomViewEngine _razorViewEngine;
private ITempDataProvider _tempDataProvider;
private IServiceProvider _serviceProvider;
private IHostingEnvironment _hostingEnvironment;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider,
IHostingEnvironment hostingEnvironment)
{
_razorViewEngine = (CustomViewEngine)razorViewEngine;
...
try
{
using (var sw = new StringWriter())
{
_razorViewEngine.RemoveViewFromCache(viewName, false);
var viewResult = _razorViewEngine.GetView(viewName, viewName, false);
These modifications do delete the ViewLookupCache using the second method. However it still doesn't properly update my views. I do have to note the views don't have their own controller.
You need to enable file watcher:
Add the environment variable DOTNET_USE_POLLING_FILE_WATCHER=true or ENV DOTNET_USE_POLLING_FILE_WATCHER=true in the Dockerfile.
If you look at the RazorViewEngine source you can see how views are cached. I'll explain how to remove a view from the cache but you'll need some knowledge about the views in order to make it work.
In your example you're looking for the view in the cache based on just a name (I'm assuming the view name). This won't work because you need to look for the view in the cache using a ViewLocationCacheKey. There are 2 constructors on the ViewLocationCacheKey struct.
public ViewLocationCacheKey(
string viewName,
bool isMainPage)
public ViewLocationCacheKey(
string viewName,
string controllerName,
string areaName,
string pageName,
bool isMainPage,
IReadOnlyDictionary<string, string> values)
In the RazorViewEngine each of these are called depending on how the view is loaded (FromPath or FromViewLocations). With my situation I have a CMS that has full Razor support where any kind of content is just a a Razor View that gets rendered through the RazorViewEngine and therefore cached in Memory. When an update is made to a piece of content then I remove that view from the ViewLookupCache and allow it to get repopulated on the next load. Unfortunately the RazorViewEngine doesn't (currently) allow you to swap out the Cache to a distributed caching option (Redis, Memcached, etc.).
Here's how I'm handle it on my custom view engine.
public void RemoveViewFromCache(string viewName, string controller, bool isLayout, bool isPartial = false, string pageName = null, string areaName = null) {
var key = new ViewLocationCacheKey(viewName, controller, areaName, pageName, !isLayout | !isPartial, isLayout ? null : new Dictionary<string, string>(StringComparer.Ordinal))
base.ViewLookupCache.Remove(key);
}
I remove the view when an update is made and let the view get reloaded and cached naturally the next time it's requested.
If your app is doing anything with ViewLocationExpanderValues you'll have to make changes. Just inspect the ViewLookupCache collection while the app is running and you'll start to get a feel for what's going on here.
In ASP.NET Core version 2.2 and greater you can do,
services.Configure<RazorViewEngineOptions>(opts => opts.AllowRecompilingViewsOnFileChange = true);
In ConfigureServices to force view engine to recompile view when it changes.
Problem:
I need to render a Razor Page partial to a string.
Why I want this:
I want to create a controller action that responds with JSON containing a partial view and other optional parameters.
Attempts:
I am familiar with the following example that renders a View to a string: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs
However, it is not compatible with Pages, as it only searches in the Views directory, so even if I give it an absolute path to the partial it tries to locate my _Layout.cshtml (which it shouldn't even do!) and fails to find it.
I have tried to modify it so it renders pages, but I end up getting a NullReferenceException for ViewData in my partial when attempting to render it. I suspect it has to do with NullView, but I have no idea what to put there instead (the constructor for RazorView requires many objects that I don't know how to get correctly).
The code:
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0
// Modified by OronDF343: Uses pages instead of views.
using System;
using System.IO;
using System.Linq;
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.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Routing;
namespace TestAspNetCore.Services
{
public class RazorPageToStringRenderer
{
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public RazorPageToStringRenderer(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderPageToStringAsync<TModel>(string viewName, TModel model)
{
var actionContext = GetActionContext();
var page = FindPage(actionContext, viewName);
using (var output = new StringWriter())
{
var viewContext = new ViewContext(actionContext,
new NullView(),
new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(),
new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions());
page.ViewContext = viewContext;
await page.ExecuteAsync();
return output.ToString();
}
}
private IRazorPage FindPage(ActionContext actionContext, string pageName)
{
var getPageResult = _viewEngine.GetPage(executingFilePath: null, pagePath: pageName);
if (getPageResult.Page != null)
{
return getPageResult.Page;
}
var findPageResult = _viewEngine.FindPage(actionContext, pageName);
if (findPageResult.Page != null)
{
return findPageResult.Page;
}
var searchedLocations = getPageResult.SearchedLocations.Concat(findPageResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find page '{pageName}'. The following locations were searched:" }.Concat(searchedLocations));
throw new InvalidOperationException(errorMessage);
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
}
This is how I did it.
As always register the Service in Startup.cs
services.AddScoped<IViewRenderService, ViewRenderService>();
The Service is defined as follows:
public interface IViewRenderService
{
Task<string> RenderToStringAsync<T>(string viewName, T model) where T : PageModel;
}
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
private readonly IHttpContextAccessor _httpContext;
private readonly IActionContextAccessor _actionContext;
private readonly IRazorPageActivator _activator;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider,
IHttpContextAccessor httpContext,
IRazorPageActivator activator,
IActionContextAccessor actionContext)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
_httpContext = httpContext;
_actionContext = actionContext;
_activator = activator;
}
public async Task<string> RenderToStringAsync<T>(string pageName, T model) where T : PageModel
{
var actionContext =
new ActionContext(
_httpContext.HttpContext,
_httpContext.HttpContext.GetRouteData(),
_actionContext.ActionContext.ActionDescriptor
);
using (var sw = new StringWriter())
{
var result = _razorViewEngine.FindPage(actionContext, pageName);
if (result.Page == null)
{
throw new ArgumentNullException($"The page {pageName} cannot be found.");
}
var view = new RazorView(_razorViewEngine,
_activator,
new List<IRazorPage>(),
result.Page,
HtmlEncoder.Default,
new DiagnosticListener("ViewRenderService"));
var viewContext = new ViewContext(
actionContext,
view,
new ViewDataDictionary<T>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
_httpContext.HttpContext,
_tempDataProvider
),
sw,
new HtmlHelperOptions()
);
var page = ((Page)result.Page);
page.PageContext = new Microsoft.AspNetCore.Mvc.RazorPages.PageContext
{
ViewData = viewContext.ViewData
};
page.ViewContext = viewContext;
_activator.Activate(page, viewContext);
await page.ExecuteAsync();
return sw.ToString();
}
}
}
I call it like this
emailView.Body = await this._viewRenderService.RenderToStringAsync("Email/ConfirmAccount", new Email.ConfirmAccountModel
{
EmailView = emailView,
});
"Email/ConfirmAccount" is the path to my Razor page (Under pages). "ConfirmAccountModel" is my page model for that page.
ViewData is null because the ViewData for the Page is set when the PageContext is set, so if this is not set ViewData is null.
I also found that I had to call
_activator.Activate(page, viewContext);
For it all to work. This is not fully tested yet so may not work for all scenarios but should help you get started.
If like me you don't get GetRouteData() from _httpContext.HttpContext and _actionContext is null, you can create an extension:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Utils
{
public static class PageExtensions
{
public static async Task<string> RenderViewAsync(this PageModel pageModel, string pageName)
{
var actionContext = new ActionContext(
pageModel.HttpContext,
pageModel.RouteData,
pageModel.PageContext.ActionDescriptor
);
using (var sw = new StringWriter())
{
IRazorViewEngine _razorViewEngine = pageModel.HttpContext.RequestServices.GetService(typeof(IRazorViewEngine)) as IRazorViewEngine;
IRazorPageActivator _activator = pageModel.HttpContext.RequestServices.GetService(typeof(IRazorPageActivator)) as IRazorPageActivator;
var result = _razorViewEngine.FindPage(actionContext, pageName);
if (result.Page == null)
{
throw new ArgumentNullException($"The page {pageName} cannot be found.");
}
var page = result.Page;
var view = new RazorView(_razorViewEngine,
_activator,
new List<IRazorPage>(),
page,
HtmlEncoder.Default,
new DiagnosticListener("ViewRenderService"));
var viewContext = new ViewContext(
actionContext,
view,
pageModel.ViewData,
pageModel.TempData,
sw,
new HtmlHelperOptions()
);
var pageNormal = ((Page)result.Page);
pageNormal.PageContext = pageModel.PageContext;
pageNormal.ViewContext = viewContext;
_activator.Activate(pageNormal, viewContext);
await page.ExecuteAsync();
return sw.ToString();
}
}
}
}
Note: this code only render the page being called and omit the layout.
You just have to call it from your PageModel like this:
var s = this.RenderViewAsync("sendEmail").Result;
"sendEmail" is the name of your PageModel view and the path is /Pages/sendEmail.cshtml
Here is the route I have gone down. Very simple and works a treat...
using System;
using System.IO;
using System.Net;
namespace gMIS.Rendering
{
public static class RazorPage
{
public static string RenderToString(string url)
{
try
{
//Grab page
WebRequest request = WebRequest.Create(url);
WebResponse response = request.GetResponse();
Stream data = response.GetResponseStream();
string html = String.Empty;
using (StreamReader sr = new StreamReader(data))
{
html = sr.ReadToEnd();
}
return html;
}
catch (Exception err)
{
return {Handle as you see fit};
}
}
}
}
Called as such....
var msg = RazorPage.RenderToString(url);
Example:
var pathToRazorPageFolder = request.PathToRazorPageFolder();
var msg = RazorPage.RenderToString($"{pathToRazorPageFolder}/Task_Summary?userGuid={userGuid}&taskId={task.Task_ID}&includelink=true&linkuserGuid={linkUserGuid}");
Above example uses this extension I created to help get the base path of my app.
namespace Microsoft.AspNetCore.Http
{
public static class RequestExtension
{
public static string PathToRazorPageFolder(this HttpRequest request)
{
if (request != null) {
var requestPath = request.Path.ToString();
var returnPathToFolder = request.Scheme + "://" + request.Host + requestPath.Substring(0, requestPath.LastIndexOf("/")); ;
return returnPathToFolder;
} else
{
return "HttpRequest was null";
}
}
}
}
I know that this doesn't use Dependency Injection, but man it's simple. And it just works. And works with any page no matter how it is hosted. Be that page be within or even outside your application.
I had the same problem.
I looked into the RazorViewEngine source code and found out that the page
is searched using the "page" route data:
var routeData = new RouteData();
routeData.Values.Add("page", "/Folder/MyPage");
It's working for me with the full path "/Folder/MyPage" in the routeData, and the page name "MyPage" in the GetPage call.