🚀 Rendering Blazor Components Dynamically with HtmlRenderer in .NET 8
A practical example using a generic component renderer
Source code available here:
👉 https://github.com/toreaurstadboss/BlazorIntroCourse/tree/main/Components/Demos/TemplatedComponents
Blazor has always been about component‑driven UI, but until .NET 8, components were tightly coupled to the Blazor runtime — either WebAssembly or Server.
With the introduction of HtmlRenderer, that boundary disappears.
You can now render any .razor component into pure HTML on the server, without a browser, without WebAssembly, and without a running Blazor app.
This opens up a whole new world of scenarios:
- Rendering components inside MVC, Razor Pages, or Minimal APIs
- Generating HTML for emails, PDFs, reports, or static site generation
- Using Blazor components as server‑side templates
- Building dynamic component renderers that choose components at runtime
- Running components in background services or unit tests
In this post, I’ll walk through a practical example:
A generic Blazor component that uses HtmlRenderer to render any component dynamically, and a simple Bootstrap‑style Alert component to demonstrate how it works.
🎯 Why HtmlRenderer Matters
Here’s the short version — arguments you can inform other developers why HtmlRenderer opens up so many possibilities for using Blazor components many places:
- Render Blazor components anywhere — MVC, Razor Pages, Minimal APIs, background jobs.
- Generate static HTML — perfect for emails, PDFs, SEO, caching, and static sites.
- Use Blazor components as templates — no need for Razor Views or TagHelpers.
- Full component lifecycle — DI, parameters, cascading values, child content all work.
- No browser required — everything runs server‑side.
- Dynamic component composition — choose components at runtime using generics.
- Great for testing — render components without a browser or JS runtime.
🖼️ Diagram: How HtmlRenderer Works
📦Sample Blazor component used | Alert.razor — A Simple Bootstrap‑Style Alert Component
<div
class=@($"alert {(IsDismissable ? "alert-dismissible fade show" : "")} alert-{AlertType.ToString().ToLower()}")
role="alert">
@if (IsDismissable)
{
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
}
@ChildContent
</div>
@code {
[Parameter]
public required RenderFragment ChildContent { get; set; } = @<b>Default message</b>;
[Parameter]
public bool IsDismissable { get; set; }
[Parameter]
public AlertTypeEnum AlertType { get; set; } = AlertTypeEnum.Success;
}
🧩 The alert type enum | AlertTypeEnum.cs
namespace DependencyInjectionDemo.Components.Demos.TemplatedComponents
{
public enum AlertTypeEnum
{
Primary,
Secondary,
Success,
Danger,
Warning,
Info,
Light,
Dark
}
}
🧠 The Generic HTML Renderer Component | GenericHtmlRenderer.razor
@((MarkupString)RenderedHtml)
@typeparam TComponent
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public Dictionary<string, object?> Parameters { get; set; } = new();
[Inject] IServiceProvider ServiceProvider { get; set; } = default!;
[Inject] ILoggerFactory LoggerFactory { get; set; } = default!;
private string RenderedHtml { get; set; } = string.Empty;
protected override async Task OnInitializedAsync()
{
if (ChildContent != null)
{
Parameters["ChildContent"] = ChildContent;
}
RenderedHtml = await RenderComponentAsync(Parameters);
}
private async Task<string> RenderComponentAsync(Dictionary<string, object?> parameters)
{
if (!typeof(IComponent).IsAssignableFrom(typeof(TComponent)))
{
throw new InvalidOperationException($"{typeof(TComponent).Name} is not a valid Blazor component.");
}
using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);
var parameterView = ParameterView.FromDictionary(parameters);
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var result = await htmlRenderer.RenderComponentAsync(typeof(TComponent), parameterView);
return result.ToHtmlString();
});
return html;
}
}
Note that we must also check if the parameters sent in here actually are present in the component. That means the parameter is a public property with attribute Parameter.
The revised code of the method RenderComponentAsync is shown below.
private async Task<string> RenderComponentAsync(Dictionary<string, object?> parameters)
{
if (!typeof(IComponent).IsAssignableFrom(typeof(TComponent)))
{
throw new InvalidOperationException($"{typeof(TComponent).Name} is not a valid Blazor component.");
}
using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);
var filteredParameters = GetValidParameters(parameters);
var parameterView = ParameterView.FromDictionary(filteredParameters);
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var result = await htmlRenderer.RenderComponentAsync(typeof(TComponent), parameterView);
return result.ToHtmlString();
});
return html;
}
private Dictionary<string, object?> GetValidParameters(Dictionary<string, object?> parameters)
{
var validnames = typeof(TComponent).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.IsDefined(typeof(ParameterAttribute), true))
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
validnames.Add("ChildContent");
var filteredParameters = parameters
.Where(kvp => validnames.Contains(kvp.Key))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var unknownKeys = parameters.Keys.Except(filteredParameters.Keys, StringComparer.OrdinalIgnoreCase).ToList();
if (unknownKeys.Count > 0)
{
var logger = LoggerFactory.CreateLogger("GenericHtmlRenderer");
string warningmsg = $"Dropped unknown component parameters for {typeof(TComponent).FullName}: {string.Join(",", unknownKeys)}";
logger.LogWarning(warningmsg);
Console.WriteLine(warningmsg);
}
return filteredParameters;
}
This adjustment makes the code more resilient in case a non-existent parameter is sent in. Please note that an invalid parameter value will still give a runtime exception,
so further validation could be added here, such as using TypeConverter to check if we can convert the parameter value sent in to the parameter in the component.
The revised code is shown below.
@using System.Reflection
@using System.ComponentModel
@using Microsoft.AspNetCore.Components
@((MarkupString)RenderedHtml)
@typeparam TComponent
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public Dictionary<string, object?> Parameters { get; set; } = new();
[Inject] IServiceProvider ServiceProvider { get; set; } = default!;
[Inject] ILoggerFactory LoggerFactory { get; set; } = default!;
private string RenderedHtml { get; set; } = string.Empty;
protected override async Task OnInitializedAsync()
{
if (ChildContent != null)
{
Parameters["ChildContent"] = ChildContent;
}
RenderedHtml = await RenderComponentAsync(Parameters);
}
private async Task<string> RenderComponentAsync(Dictionary<string, object?> parameters)
{
if (!typeof(Microsoft.AspNetCore.Components.IComponent).IsAssignableFrom(typeof(TComponent)))
{
throw new InvalidOperationException($"{typeof(TComponent).Name} is not a valid Blazor component.");
}
using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);
var filteredParameters = GetValidParameters(parameters);
var parameterView = ParameterView.FromDictionary(filteredParameters);
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var result = await htmlRenderer.RenderComponentAsync(typeof(TComponent), parameterView);
return result.ToHtmlString();
});
return html;
}
private Dictionary<string, object?> GetValidParameters(Dictionary<string, object?> parameters)
{
var componentType = typeof(TComponent);
// Get valid property names and their PropertyInfo
var validProperties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.IsDefined(typeof(ParameterAttribute), true))
.ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase);
// Add ChildContent as a special case
validProperties["ChildContent"] = null!;
var filteredParameters = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in parameters)
{
if (validProperties.ContainsKey(kvp.Key))
{
var propertyInfo = validProperties[kvp.Key];
if (propertyInfo != null)
{
var targetType = propertyInfo.PropertyType;
var value = kvp.Value;
// Check if value is null or already assignable
if (value == null || targetType.IsInstanceOfType(value))
{
filteredParameters[kvp.Key] = value;
}
else
{
// Use TypeDescriptor to check conversion
var converter = TypeDescriptor.GetConverter(targetType);
if (converter != null && converter.CanConvertFrom(value.GetType()))
{
try
{
filteredParameters[kvp.Key] = converter.ConvertFrom(value);
}
catch
{
LogConversionWarning(kvp.Key, value);
}
}
else
{
LogConversionWarning(kvp.Key, value);
}
}
🧪 Demo Page — Rendering Alerts Dynamically | GenericHtmlRendererDemo.razor
@page "/GenericHtmlRendererDemo"
@using BlazorIntroCourse.Components.Demos.TemplatedComponents
<h3>GenericHtmlRendererDemo - Dynamic Alert rendering</h3>
<h6>Here the parameters are set in code</h6>
<GenericHtmlRenderer TComponent="Alert"
ChildContent="alertContent"
Parameters="alternateAlertParameters" />
<h6>Here we use the ChildContent element to enter in the html</h6>
<GenericHtmlRenderer TComponent="Alert"
Parameters="alertParameters">
<ChildContent>
<text>This is the second alert</text>
</ChildContent>
</GenericHtmlRenderer>
<h6>Here we use inline object initializer for parameters</h6>
<GenericHtmlRenderer TComponent="Alert"
Parameters="@(new Dictionary<string, object>{
[ "AlertType" ] = AlertTypeEnum.Danger,
[ "IsDismissable" ] = true
})">
<ChildContent>
<text>This is the third alert</text>
</ChildContent>
</GenericHtmlRenderer>
@code {
private RenderFragment alertContent = @<text>This is the first Alert</text>;
Dictionary<string, object?> alertParameters = new()
{
{ "AlertType", AlertTypeEnum.Info},
{ "IsDismissable", true }
};
Dictionary<string, object?> alternateAlertParameters = new()
{
{ "AlertType", AlertTypeEnum.Success },
{ "IsDismissable", true }
};
}
📘 Diagram: Where You Can Use HtmlRenderer
🏁 Wrapping Up
HtmlRenderer in .NET 8 is one of those features that quietly unlocks a huge amount of flexibility.
It turns Blazor components into universal UI building blocks that can be rendered:
- in a browser
- on the server
- inside MVC
- inside Razor Pages
- inside Minimal APIs
- inside background services
- inside unit tests
- or even at build time
The generic component renderer shown here is a concrete example of how powerful this can be — dynamic component rendering, runtime composition, and server‑side HTML generation all in one.
If you’re curious, you can explore the full source code here: (part of a larger repo I am looking into currently as a playground for misc Blazor functionality):
👉 https://github.com/toreaurstadboss/BlazorIntroCourse/tree/main/Components/Demos/TemplatedComponents




