Sometimes you may need to render a certain view as string, for example when you want to send an email where usually the body is pure html.

To do so, you need to create a service that takes two arguments:

  • the first one is a string representing the path to the view you want to render;
  • the second one is an object that is used from the view.
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 Project.Utilities
{
    public interface IViewRenderService
    {
        Task<string> RenderToStringAsync(string viewName, object model);
    }

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

            using (var sw = new StringWriter())
            {
                var viewResult = _razorViewEngine.FindView(actionContext, 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);
                return sw.ToString();
            }
        }
    }
}

Register Service

ASP.NET Core is designed from the ground up to support and leverage dependency injection. In the Startup class you can register application services that can be configured for injection throughout your application.

To do so, you have to add your `IViewRenderService` in `IServiceCollection` services. The first generic type represents the type (typically an interface) that will be requested from the container. The second generic type represents the concrete type that will be instantiated by the container and used to fulfill such requests.

We have selected `Scoped` over `Transient` as Scoped objects, which are the same within a request, but different across different requests

public void ConfigureServices(IServiceCollection services)
{
    // Previous configuragion
    // ...

    // Add Applciation Services
    services.AddScoped<IViewRenderService, ViewRenderService>();
}

Example

Let’s assume you have a very simple View under path `Views/Email/Invite.cshtml` which uses an `InviteViewModel`; you may then create a controller that takes IViewRenderService as a parameter:

[Route("render")
public class RenderController : Controller
{
	private readonly IViewRenderService _viewRenderService;

	public RenderController(IViewRenderService viewRenderService)
	{
		_viewRenderService = viewRenderService;
	}

	[Route("invite")]
	public async Task<IActionResult> RenderInviteView()
	{
		var viewModel = new InviteViewModel
		{
			UserId = "cdb86aea-e3d6-4fdd-9b7f-55e12b710f78",
			UserName = "iggy",
			ReferralCode = "55e12b710f78",
			Credits = 10
		};

		var result = await _viewRenderService.RenderToStringAsync("Email/Invite", viewModel);
		return Content(result);
	}
}

Following the above, if you call {URL}/render/invite the response will be the `Email/Invite` view, rendered as string.

Categorized in:

Tagged in: