I am attempting to generate an HTML email from a Razor page. This works well for HTML without any model. However when I add a model "WelcomeEmailModel" and attempt to pass a variable, I get the following error on the '@Model.UserName' parameter.
'Object reference not set to an instance of an object.'
The model is not being passed to the view.
I can see in the ViewData (see image below), that the Model 'Username' is populated with "Jane Doe".
The examples I have found via google do not include a model. Is it possible to send the model this way or should the model be passed another way?
Razor View
@page
@model WelcomeEmailModel
<!DOCTYPE html>
<html>
<head>
<title>Welcome to Our Website!</title>
</head>
<body>
<h2>Welcome, @Model.UserName!</h2>
<p>Thank you for signing up to our website. We're excited to have you on board!</p>
</body>
</html>
EmailService.cs
namespace FlightLog.Services
{
public class EmailService
{
private readonly EmailClient _emailClient;
private readonly string _senderEmailAddress;
private readonly IRazorViewEngine _razorViewEngine;
private readonly IServiceProvider _serviceProvider;
public EmailService(string connectionString, string senderEmailAddress, IRazorViewEngine razorViewEngine, IServiceProvider serviceProvider)
{
_emailClient = new EmailClient(connectionString);
_senderEmailAddress = senderEmailAddress;
_razorViewEngine = razorViewEngine;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
{
var actionContext = GetActionContext();
var viewEngineResult = _razorViewEngine.FindView(actionContext, viewName, false);
//var viewEngineResult = _razorViewEngine.GetView("~/Views/EmailTemplates", $"{viewName}.cshtml", false);
if (!viewEngineResult.Success)
{
throw new InvalidOperationException($"Unable to find view '{viewName}'");
}
using (var sw = new StringWriter())
{
var tempDataSerializer = (TempDataSerializer)_serviceProvider.GetService(typeof(TempDataSerializer));
var tempDataProvider = new SessionStateTempDataProvider(tempDataSerializer);
var viewContext = new ViewContext(
actionContext,
viewEngineResult.View,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(actionContext.HttpContext, tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewEngineResult.View.RenderAsync(viewContext);
return sw.ToString();
}
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
public async Task SendEmailAsync(string recipient, string subject, string body)
{
var emailContent = new EmailContent(subject)
{
Html = body
};
var emailMessage = new EmailMessage(
_senderEmailAddress,
recipient,
emailContent);
await _emailClient.SendAsync(WaitUntil.Completed, emailMessage, CancellationToken.None);
}
public async Task SendWelcomeEmailAsync(string recipient, string userName)
{
var viewModel = new WelcomeEmailModel
{
UserName = userName
};
string emailBody = await RenderViewToStringAsync("WelcomeEmail", viewModel);
await SendEmailAsync(recipient, "Welcome to Our Website!", emailBody);
}
}
}

Be sure remove the
@pagein your razor view because your customRenderViewToStringAsyncmethod is used to render razor view instead of razor pages to string.The difference between razor view and razor pages:
Razor Pages can make coding page-focused scenarios easier and more productive than using controllers and views. What makes it different is the
@pagedirective.@pagemakes the file into an MVC action, which means that it handles requests directly, without going through a controller.@pagemust be the first Razor directive on a page.@pageaffects the behavior of other Razor constructs.