I'm adding an updated answer to this using tools from later versions of .Net (8 and 9). The previous solution from @enet served well, but the new approach is done completely in C# without needing to rely on JS or browser rendering at all. Much more streamlined.
It relies on HtmlRenderer
, which takes Razor component (componentType
in this example) and a dictionary of all parameters. It also needs access to both your service provider and logger factory.
This is the invoking method I use throughout my apps, usable both as a <T>
generic and with Type
as a parameter (with some simplifications for this example).
public static async Task<string> RenderTemplate<T>(Dictionary<string, object?> parameters, IServiceProvider serviceProvider) where T : IComponent, IEmailTemplateComponent
{
return await RenderTemplate(typeof(T), parameters, serviceProvider);
}
public static async Task<string> RenderTemplate(Type componentType, Dictionary<string, object?> parameters, IServiceProvider serviceProvider)
{
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException($"Type {componentType.Name} must implement IComponent.", nameof(componentType));
}
ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
await using var htmlRenderer = new HtmlRenderer(serviceProvider, loggerFactory);
return await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var parameterView = ParameterView.FromDictionary(parameters);
var output = await htmlRenderer.RenderComponentAsync(componentType, parameterView);
string htmlString = output.ToHtmlString();
return htmlString;
});
}
Here is an example method call:
string renderedEmail = await ComponentRenderer.RenderTemplate<InspectionEmailTemplate>(
new Dictionary<string, object?>() { { nameof(InspectionEmailTemplate.Project), project },
{ nameof(InspectionEmailTemplate.Inspection), inspection},
{ nameof(InspectionEmailTemplate.SubmittingUserFullName), args.SubmittingUser},
{ nameof(InspectionEmailTemplate.comment), args.comment} },
_serviceProvider);
See docs for more details: