In a previous post, ]I wrote about using RazorComponentResult to render Blazor components](/how-to-use-blazor-server-rendered-components-with-htmx) from ASP.NET Core Minimal APIs. The ability allows developers to reuse Blazor components in new and exciting scenarios, specifically with JavaScript UI frameworks and libraries such as React, Vue, Angular, and my favorite library, HTMX.

In this concise post, we’ll explore setting HTTP Headers for RazorComponentResult and creating an extension method that simplifies this task, making your development process more efficient.

RazorComponentResult Recap

Blazor is a component-driven development framework inspired by the JavaScript React library. Components aim to encapsulate UI elements into reusable elements to help accelerate development. They can vary in size, from buttons, links, and textboxes to logical components such as detail cards, tables, video elements, and so on.

Component trees also help manage a page’s state, and Blazor provides some DOM diffing capabilities similar to React. The aim is to make pages more responsive and performant to user interactions without unnecessary DOM swaps. This feature expects components to be nested in other components until you reach the parent component, typically a page. But what about using Blazor for HTML fragments?

RazorComponentResult allows ASP.NET Core endpoints to generate HTML server-side from Blazor components. This can help build HTML APIs for other JavaScript frameworks or vanilla JavaScript calls from your client UI. Let’s see what it looks like.

app.MapGet("/vanilla",
    () => new RazorComponentResult<LoveHtmx>(new
    {
        Message = "I ❤️ ASP.NET Core"
    }));

The corresponding component of LoveHtmx is what you’d expect a Blazor component to look like.

<div class="alert alert-info">
    <span class="text-lg-center">
        @Message
    </span>
</div>

@code{
    [Parameter]
    public string? Message { get; set; } = "I ❤️ HTMX";
}

Cool! Now that we’re all caught back up, how do we set these HTTP headers?

HttpContext and Server-Side Blazor Components

RazorComponentResult generates the HTML that would accompany a Blazor component. While it’s essential to recognize this approach’s limitations, those limitations allow us to make some advantageous assumptions.

  • Blazor Components must be completely rendered on the server, with no interactivity. I’m sorry, but you’re not getting SignalR or Wasm here.
  • RazorComponentResult manages the entire Response lifecycle.
  • You need to specify the state of the component at render time.
  • You get access to all the server goodies, like HttpContext.

OK, let’s first do the wrong thing. This does not work ❌.

app.MapGet("/vanilla",
    (HttpContext ctx) =>
    {
        ctx.Response.Headers.Append("Nope", "Fail");
        
        return new RazorComponentResult<LoveHtmx>(new
        {
            Message = "I ❤️ ASP.NET Core"
        });
    });

This will fail if you try to set HTTP headers from your ASP.NET Core Minimal API endpoint. Remember, the RazorComponentResult manages the entire Response state. How do we get what we want?

First, we must add an IHttpContextAccessor to our services collection in our Program file.

builder.Services.AddHttpContextAccessor();

Now, let’s get to the component. We need to set headers in the component using the HttpContextinstance, which we’ll get from an IHttpContextAccessor. We can inject the instance using the InjectAttribute or the @inject declaration.


<div class="alert alert-info">
    <span class="text-lg-center">
        @Message
    </span>
</div>

@code{
    [Parameter] public string? Message { get; set; } = "I ❤️ HTMX";

    [Parameter]
    public IHeaderDictionary? Headers { get; set; }
        = new HeaderDictionary();

    [Inject] public IHttpContextAccessor? HttpContextAccessor { get; set; }

    protected override Task OnInitializedAsync()
    {
        if (HttpContextAccessor?.HttpContext is { } ctx &&
            Headers is not null)
        {
            foreach (var (key, value) in Headers)
            {
                ctx.Response.Headers.Append(key, value);
            }
        }

        return base.OnInitializedAsync();
    }
}

Or you might prefer this approach.

@inject IHttpContextAccessor HttpContextAccessor

<div class="alert alert-info">
    <span class="text-lg-center">
        @Message
    </span>
</div>

@code{
    [Parameter] public string? Message { get; set; } = "I ❤️ HTMX";

    [Parameter]
    public IHeaderDictionary? Headers { get; set; }
        = new HeaderDictionary();

    protected override Task OnInitializedAsync()
    {
        if (HttpContextAccessor?.HttpContext is { } ctx &&
            Headers is not null)
        {
            foreach (var (key, value) in Headers)
            {
                ctx.Response.Headers.Append(key, value);
            }
        }

        return base.OnInitializedAsync();
    }

}

This component will only work as a server-rendered component since HttpContext is a dependency.

We can now also write an excellent extension method to keep us from making previous mistakes in the future.

public static class RazorComponentResultExtensions
{
    public static RouteHandlerBuilder MapGetRazorComponent<TComponent>(
        this IEndpointRouteBuilder endpoints,
        [StringSyntax("Route")] string pattern,
        object? state = null
    )
        where TComponent : IComponent
    {
        var dictionary = new RouteValueDictionary(state);
        return endpoints.MapGet(pattern, () => new RazorComponentResult<TComponent>(dictionary));
    }
}

And our new registration now looks like this.

app.MapGetRazorComponent<LoveHtmxWithHeader>(
    "/love-htmx",
    new
    {
        Message = "I ❤️ ASP.NET Core",
        Headers = new HeaderDictionary
        {
            { "Hx-Trigger", "blazor-x" }
        }
    });

Conclusion

There you have it. You can now set HTTP headers when using RazorComponentResult. Using the extension method I provided also makes it clear not to add any additional code in your endpoints, helping you avoid the chance of introducing bugs. Give it a shot!

As always, thanks for reading and sharing my posts with friends and colleagues. As always, cheers.