94

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.

Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
Christof Jans
  • 1,854
  • 1
  • 17
  • 10

7 Answers7

55

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...
        }
    }
}
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • 2
    Nice work, thanks! In order to get it working in netstandard 2.0 class library running in netcore2 app on mac and linux I had to add an additional reference to netstandard dll: `MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"netstandard.dll")),` – Isantipov Dec 19 '17 at 11:03
  • 2
    @Isantipov - ok, thanks for pointing that out, I had not tested this on other platforms than Windows. I've updated the answer. – Simon Mourier Dec 19 '17 at 17:37
  • Would the razor engine automatically find the "_ViewImports.cshtml" file, or do I have to locate the layout, view imports, etc., files myself going this route? Also, what about the 'ViewContext' and other related properties in the views - how does the razor engine know how to set those? Or are they null? – James Wilkins Feb 26 '18 at 17:43
  • @JamesWilkins - As the name implies, these are View concepts, so, they're not in the Razor language itself, but in ASP.NET MVC (since View is V). – Simon Mourier Feb 26 '18 at 19:27
  • It's what I thought, thanks. ;) I wasn't sure since the main point of Razor is compiling views (aka cshtml files). I wasn't sure if searching for other specially named files was part of the engine's process, and not just MVC. Doesn't hurt to ask to be sure. ;) – James Wilkins Feb 26 '18 at 20:51
  • @JamesWilkins - historically, the main point of razor was to compete with PHP, ie: give developers an HTML templatin language more simple than WebForms (aspx, ascx) spaghetti. It's still spaghetti, but the cook is - supposedly - better :-) It's more a "view engine" if you like, but the view itself is provided by an external system (=> MVC). See also https://stackoverflow.com/questions/11380607/asp-net-webforms-vs-asp-net-websiterazor-vs-asp-net-mvc – Simon Mourier Feb 27 '18 at 07:02
  • 2
    Well if I recall, the actual RazorViewEngine is in MVC, so I guess that makes Razor nothing more than a parser and compiler I guess. ;) – James Wilkins Feb 27 '18 at 07:27
  • @JamesWilkins - more or less, yes – Simon Mourier Feb 27 '18 at 07:31
  • Does anyone have the updated example of this for .NET Core 2.1? I am getting: warning CS0618: 'InheritsDirective.Register(IRazorEngineBuilder)' is obsolete: 'This method is obsolete and will be removed in a future version.' warning CS0618: 'RazorEngine.Create(Action)' is obsolete: The recommended alternative is RazorProjectEngine.Create' Also when running: error CS0234: The type or namespace name 'AspNetCore' does not exist in the namespace 'Microsoft' – Dave Jul 17 '18 at 01:10
  • 1
    @Dave - I have updated my answer. It should work with the newest versions now. – Simon Mourier Jul 20 '18 at 12:39
  • Hi Simon, So would it be possible to create an MVC app with compiled Razor Views and then use your technique to load the WebApplication1.Views.dll file and use ExecuteAsync on one of the Views? I tried this and it also needed the WebApplication1.dll reference which I added. I ended up trying: dynamic template = Activator.CreateInstance(asm.GetType("WebApplication1.Pages.Pages_About")); template.ExecuteAsync().Wait(); But I get {System.NullReferenceException: Object reference not set to an instance of an object. at WebApplication1.Pages.Pages_About.ExecuteAsync()} – Dave Jul 21 '18 at 23:53
  • @Dave - maybe, not sure. You should ask another question (with full code and stacktrace). – Simon Mourier Jul 22 '18 at 07:25
  • @SimonMourier you have some sort of typo in your code, look for `??)` – Mehdi Dehghani Aug 23 '18 at 04:43
  • @MehdiDehghani - Thanks, some copy/paste issue, thought I'd found them all :-) – Simon Mourier Aug 23 '18 at 06:02
  • @daniherrera - I just tested it with fresh .net core 3 and .net framework c# console apps, latest package versions and it works fine. Make sure you copied the whole code, including the namespace ("RazorTemplate", etc.). – Simon Mourier May 15 '19 at 21:41
  • @SimonMourier You mentioned in a comment that "'Template' is the type name that razor uses by default," is there any way to change that? – Merlin04 Jul 07 '20 at 18:51
  • @Merlin04 - I can't test that right now. Maybe try to ask another question – Simon Mourier Jul 08 '20 at 04:32
  • FYI: I could not get my view to inherit from the base template class, both with and without the `InheritDirective` line. – Tanveer Badar Sep 03 '20 at 14:17
  • @TanveerBadar - not sure what you mean by "view", there's no view here. I've tested the code and it still works. Make sure you test with the exact code. – Simon Mourier Sep 03 '20 at 14:53
50

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" }); 
Toddams
  • 1,489
  • 15
  • 23
  • 3
    This was pretty easy to implement however it has pretty terrible performance. I created a loop that generated around a 1000 lines of html. It took around 12 seconds everytime. Just creating a single page with 200 lines took about 1-2 seconds. In an MVC project 1 page took about 20 milliseconds. So If you are not worried about performance this is a viable option. – DeadlyChambers Jul 07 '17 at 13:32
  • Well, if you use ParseString - templates are not cached, that's why you experience performance issues. Use Parse instead with appropriate template manager (for files or embedded resources) - this way template will only be compiled once and next time will be taken from cache. And you'll see the same numbers as in MVC project – Toddams Sep 19 '17 at 08:24
  • 9
    Update: 2.0 version caches templates built from strings – Toddams Dec 28 '17 at 11:25
  • 2
    @Toddams This is not going to work on production because views are precompiled on publish. Can you please add mvc project sample which support production(precompiled views) and development environment. I am not able to make it work for both environments together :(( – Freshblood Jun 19 '18 at 13:52
  • 1
    @Toddams I'm in the same boat for the precompiled views; _RazorLight_ is not working at all when we're using `dotnet publish` to build the app. – bchhun Apr 15 '19 at 20:08
  • @Toddams; What are your timelines wrt netstandard 2.1 and netcore 3.0? – JDeVil Apr 29 '19 at 07:26
  • 2
    Unfortunately this has stopped working with .net5, specifically with Microsoft.Extensions.Primitives5.0.0. The error that occurs is 'Could not load type 'Microsoft.Extensions.Primitives.InplaceStringBuilder' – milorad May 24 '21 at 08:35
  • 4
    That should not be the accepted answer IMHO. Razor is a product Razor markup language is a markup language used by the Razor engine which is part of Razor product. So if it was asked how to use Razor ML, that may be the answer, but Chrostof asked how to use Razor, that is a MS product. The answer here does not explain, only tell how to use that custom product that claim to support Razor ML has Razor do (but no guarantee about that nor any guarantee is given about support for the future). – Skary May 25 '21 at 13:00
32

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);
Alexander Selishchev
  • 1,180
  • 13
  • 29
20

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>
razon
  • 3,882
  • 2
  • 33
  • 46
Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
  • Hi Nate, quick question, I thought that `view.RenderAsync(viewContext).GetAwaiter().GetResult();` should be avoided and async/await should be used or was there a rationale why you wrote this example synchronous? – dustinmoris Feb 11 '17 at 00:44
  • @dustinmoris Thanks for pointing that out, it was an oversight on my part. You're absolutely right. I've updated the code. The console demo still calls `GetAwaiter().GetResult()` because of the lack of proper async console support. :) – Nate Barbettini Feb 11 '17 at 01:43
  • Hi @nate-barbettini, I have another quick question, does the RazorViewEngine do any optimisations so that the same view doesn't get compiled each time from scratch or would I have to create my own layer of caching or what not do speed up the rendering of razor pages in a non MVC web application? – dustinmoris Mar 26 '17 at 10:50
  • 2
    @dustinmoris If I recall correctly, it does do some caching for you. I haven't tried it in a while. – Nate Barbettini Mar 26 '17 at 17:17
  • 2
    Awesome! But, this doesn't work in .net core 2.0 :( It seems that the dependencies can't be loaded: `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'` - I'm not sure how to tell razor to load all the dependencies it needs - any ideas? – Matt Roberts Sep 06 '17 at 11:09
  • Just in case this helps anyone I've managed to get this working is .net core 2.0 (I didn't get the attribute issue) but I did hit some funny things as I was adding a view to an API (i.e. there were no views to start with). When I added the razor page it also included a code behind file which you have to remove and then create a separate model. Then I kept getting Model null references and to solve this I had to remove the "@page" line from the top of the razor page. Then everything started working. – Ben Thomson Sep 22 '17 at 01:35
  • @MattRoberts did you ever solve this? facing the same issue here – zaitsman Oct 03 '17 at 03:49
  • I posted it as another answer. – ArcadeRenegade Oct 07 '17 at 00:10
  • What if you don't want this working as part of an asp.net core project, but a standalone console app? Seems like there's a lot of baggage and dependencies just to render Razor pages. – Jeremy Holovacs Nov 25 '17 at 14:49
  • @JeremyHolovacs The example in my answer is a console app. – Nate Barbettini Nov 25 '17 at 21:33
  • Top answer given it doesn't solicit a library. – pim Jan 24 '18 at 00:51
  • @NateBarbettini nice example, any idea how I can add caching (reduce I/O or anything can boost the performance of it) to your code? – Mehdi Dehghani Feb 23 '18 at 08:43
  • @MehdiDehghani IIRC, the `ViewEngine.FindView` method does some internal caching, so finding and rendering a view the second time is a little faster. What performance issue are you having? – Nate Barbettini Feb 23 '18 at 20:42
  • 1
    This code appears to leak memory in our project, but I'm not certain what the exact culprit is. – Doug Mar 08 '18 at 17:30
  • @NateBarbettini on first run, the `RazorViewToStringRenderer` took ~2240ms and for second and 3rd (and I think 4rd and ...) it took ~320ms, so seems like just first time needs big time, but still I'm looking for better performance here, specially @ first time, btw there was a really simple testcase, is there any way to improve this times? – Mehdi Dehghani Mar 24 '18 at 10:39
  • 1
    @MehdiDehghani Razor always takes a long time to compile on the fly (the first time). You might be interested in the new Razor library support and compile-at-build feature in ASP.NET Core 2.1: https://blogs.msdn.microsoft.com/webdev/2018/02/02/asp-net-core-2-1-roadmap/ – Nate Barbettini Mar 24 '18 at 22:47
  • 1
    @doug Yes, we are seeing a memory leak too and no way to reclaim the memory (no disposables in use). With a large model (80,000 rows of products for an XML feed) this kills the server after a few runs. – Ryan O'Neill Apr 03 '18 at 13:15
  • @NateBarbettini I'm sorry to asking another question, I'm using your code now in my final project, everything is fine, thanks to you, but there is one problem, if I make any changes to `.cshtml`, by refreshing the browser I can not see the changes, I have to restart the server to see the changes. btw I'm using `context.Response.WriteAsync(...)` to send data to browser. – Mehdi Dehghani Apr 14 '18 at 17:37
  • @MehdiDehghani I believe `IRazorViewEngine.FindView` performs caching, so that may be why you aren't seeing changes on refresh. I definitely wouldn't call this code production-ready, it was just an example. Is there a reason you can't use ASP.NET Core to render browser responses? That seems much more straightforward. – Nate Barbettini Apr 14 '18 at 18:10
  • @NateBarbettini Really thanks for your attention here. yes, I can not do that, I choose .Net core, because this is the only way to do cross-platform using C#, I'm working on some sort of site generator and I use Razor to render templates. so there is no compile/build step for end user (like normal MVC project). is there any way to disable that cache?or I have to search for another template engine? btw your code here really work well. – Mehdi Dehghani Apr 15 '18 at 04:26
  • @MehdiDehghani I don't know, sorry. You'd have to go digging through the Razor source: https://github.com/aspnet/Razor – Nate Barbettini Apr 16 '18 at 02:13
  • 2
    For anyone getting the `The type 'Attribute' is defined in an assembly that is not referenced` error, adding `true` resolved the issue. – Mark G May 09 '18 at 04:00
  • 4
    Note that for use with ASP.NET Core 3 you need to add an `IWebHostEnvironment` instance to the DI container, and use the `MvcRazorRuntimeCompilationOptions` class to provide file providers instead of the `RazorViewEngineOptions` class. An example detailing `RazorViewToStringRenderer` instantiation is available here: http://corstianboerman.com/2019-12-25/using-the-razorviewtostringrenderer-with-asp-net-core-3.html. – Corstian Boerman Dec 25 '19 at 14:47
  • @ryan-oneill did u find the cause of the leak? any fix you can advice? – acromm Feb 05 '20 at 18:06
8

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();
    }
}
ArcadeRenegade
  • 804
  • 9
  • 14
  • I am not able to get this to work. I get the error: Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.Razor.IRazorViewEngine' while attempting to activate 'Mvc.RenderViewToString.RazorViewToStringRenderer'.' – Kjensen May 11 '18 at 21:01
  • 1
    This is a very nice and simple answer and mostly great because it works in Linux backed Docker images. A lot of other solutions do not work due to some Linux specific issues.... This one does. Thank you! This should be the answer for ASPNET CORE 2+ – Piotr Kula Dec 19 '18 at 14:33
  • Does this use caching at all? – jjxtra Feb 24 '19 at 16:04
4

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.

Soundar Anbu
  • 129
  • 5
0

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)
        {
        }
    }
}
jjxtra
  • 20,415
  • 16
  • 100
  • 140
  • 1
    Using `Microsoft.NET.Sdk.Web` means the project will require ASP.NET Core runtime in order to run. This could be very heavy and unnecessary for console/desktop apps. – Tyrrrz Jul 28 '20 at 21:47
  • In my experience the bloat is fairly minimal. A self contained .exe as of .net core 3.1 with entire asp.net core runtime and trimming enabled is about 80mb. If you are creating an installer, these files will get zipped down to around 30mb and extracted back out. If you have a microservice, then it's a non-issue. For a stand-alone console app, I would suggest switching to .net 5 self-contained exe, otherwise you will have a lot of extra dll files and folders. – jjxtra Jul 29 '20 at 01:56