Render Partial View to String in ASP.NET MVC (Classic + .NET 8/9)
Render a Razor partial view to a string instead of a full HTTP response — essential for generating email bodies, returning HTML fragments from AJAX endpoints, and server-rendering components for hybrid SPA pages. The pattern has evolved from MVC 5’s ViewEngines.Engines to .NET 8/9’s ICompositeViewEngine, but the use cases are the same.
I first wrote this in 2012 for a B2B app where the confirmation email needed to match the in-app order summary exactly. Duplicating the HTML between the view and the email template was a maintenance nightmare — every change had to happen twice. Rendering the partial to a string and piping it into the email body solved it. The same pattern reappears constantly: AJAX endpoints returning HTML fragments (before HTMX made that elegant), server-side rendering for client frameworks, even generating PDFs with libraries that accept HTML input.
What this pattern does
- Renders a Razor partial view with a model to a string value
- Works inside controller actions, background jobs, and email services
- Respects the full Razor pipeline — tag helpers, view components,
@Htmlhelpers all execute normally - Returns HTML-encoded safe output (same as a regular view render)
- Supports view component rendering too (
IViewComponentHelper) - Cleanly split into an extension method so you write
await this.RenderViewToStringAsync("Partial", model)
Install and use
For MVC 5 (.NET Framework 4.x): the old ViewEngines-based helper. For .NET 8/9: inject ICompositeViewEngine via DI. Both patterns shown below. Wire the extension method into a base controller or a utility class.
// ============================================================
// LEGACY: ASP.NET MVC 5 (.NET Framework 4.x)
// ============================================================
public static class ControllerExtensions
{
public static string RenderPartialViewToString(this Controller c,
string viewName,
object model)
{
c.ViewData.Model = model;
using (var sw = new StringWriter())
{
var vr = ViewEngines.Engines.FindPartialView(c.ControllerContext, viewName);
var ctx = new ViewContext(c.ControllerContext, vr.View, c.ViewData, c.TempData, sw);
vr.View.Render(ctx, sw);
vr.ViewEngine.ReleaseView(c.ControllerContext, vr.View);
return sw.GetStringBuilder().ToString();
}
}
}
// ============================================================
// MODERN: ASP.NET Core MVC / Razor Pages (.NET 8/9)
// ============================================================
public interface IRazorViewToStringRenderer
{
Task<string> RenderAsync<TModel>(string viewName, TModel model);
}
public class RazorViewToStringRenderer : IRazorViewToStringRenderer
{
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _services;
public RazorViewToStringRenderer(IRazorViewEngine v,
ITempDataProvider t,
IServiceProvider s)
{
_viewEngine = v;
_tempDataProvider = t;
_services = s;
}
public async Task<string> RenderAsync<TModel>(string viewName, TModel model)
{
var actionContext = GetActionContext();
var view = _viewEngine.FindView(actionContext, viewName, isMainPage: false).View
?? throw new InvalidOperationException($"View {viewName} not found.");
await using var sw = new StringWriter();
var viewData = new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(),
new ModelStateDictionary()) { Model = model };
var tempData = new TempDataDictionary(actionContext.HttpContext, _tempDataProvider);
var viewContext = new ViewContext(actionContext, view, viewData, tempData, sw, new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return sw.ToString();
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext { RequestServices = _services };
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
// Program.cs — register the service
builder.Services.AddSingleton<IRazorViewToStringRenderer, RazorViewToStringRenderer>();
// Use it
public class EmailService(IRazorViewToStringRenderer renderer)
{
public async Task SendOrderEmail(Order order)
{
var html = await renderer.RenderAsync("Emails/OrderConfirmation", order);
// pass to SMTP / SendGrid / Mailjet
}
}How the modern renderer works
ASP.NET Core’s IRazorViewEngine normally runs inside the request pipeline with a full HttpContext. To render outside a request (a background job, a scheduled email), you synthesize the minimum pieces Razor needs: an ActionContext with an empty RouteData, an HttpContext with the app’s DI container, a ViewDataDictionary holding the model, and a StringWriter to capture output. view.RenderAsync executes the full Razor pipeline — tag helpers, view components, injected services all resolve normally through the synthesized RequestServices. The result is identical to what the user would see if they visited the view at an HTTP endpoint.
2026 recommendations
- Email templates — the primary use case. Pair this with MailKit or FluentEmail for the SMTP side
- HTMX endpoints — return HTML fragments directly from Razor views, HTMX swaps them into the DOM. HTMX + Razor is a dream stack for server-rendered interactivity in 2026
- PDF generation — render the Razor view to a string, feed into DinkToPdf or iText7 for PDF output
- Preview/Approval flows — render the final email to string, show in an in-app iframe before sending
- Background jobs — Hangfire or Quartz.NET jobs can inject IRazorViewToStringRenderer and send emails asynchronously
FAQs
Does this work with view components?
Yes with extra setup. Inject IViewComponentHelper, call helper.InvokeAsync("ComponentName", arguments), write to a StringWriter. The principle is identical to partial views — synthesize the context, render to a writer, return the string.
Will injected services work inside the rendered view?
Yes — RequestServices on the synthesized HttpContext is set to the app’s service provider, so @inject directives in the view resolve normally. Scoped services get a new scope per render call, which is usually what you want.
Can I use this from a hosted service / Worker Service?
Yes. A hosted service doesn’t have a request context, but as long as you register IRazorViewEngine, ITempDataProvider, and the IHtmlHelper-related services (via builder.Services.AddControllersWithViews()), the renderer works. You may need to register IHttpContextAccessor as well if any views use it.
Is there a NuGet package that does this?
RazorLight is the most popular one. It’s a standalone Razor compiler without the MVC pipeline — faster for pure templating, but you lose tag helpers and DI resolution. For anything that needs the full Razor experience, roll your own with the pattern above.
What about Blazor components?
Blazor components render differently — use HtmlRenderer from Microsoft.AspNetCore.Components.Web to render a Blazor component to string. New in .NET 8, and it’s the right answer for Blazor-first apps that want to generate HTML for emails.
Performance: caching the rendered string?
Razor compiles views to IL on first render and caches the compiled assembly — subsequent renders are fast. If your template is fully static (no model-dependent rendering), cache the final string in IMemoryCache keyed on the model hash.