Head Content Injection in .NET 8 Blazor Web Apps

Head Content Injection in .NET 8 Blazor Web Apps

My journey with Microsoft Semantic Kernel marked the beginning of a new adventure: stepping out of my comfort zone as a backend developer to create applications with user interfaces, rather than just building apps for unit and integration testing.

I naturally chose Blazor as my UI framework, and I’ll be sharing my frontend development experiences here. Sometimes it can be frustratingly difficult to accomplish seemingly simple tasks (like centering a div!), but AI assistants like GitHub Copilot have been incredibly helpful in reducing those pain points.

One of my recent challenges involved programmatically including JavaScript and CSS in Blazor applications. I prefer an automated approach rather than manually adding tags to HTML. Back in the .NET 5 era, I wrote an article about using tag helpers for this purpose, which you can find here

However, I recently discovered that my original approach no longer works. I’ve been developing several prototypes using the new DevExpress Chat component, and many of these prototypes include custom components that require JavaScript and CSS. Despite my attempts, I couldn’t get these components to work with the tag helpers, and the reason wasn’t immediately obvious. During the Thanksgiving break, I decided to investigate this issue, and I’d like to share what I found.

With the release of .NET 8, Blazor introduced a new web app template that unifies Blazor Server and WebAssembly into a single project structure. This change affects how we inject content into the document’s head section, particularly when working with Tag Helpers or components.

Understanding the Changes

In previous versions of Blazor, we typically worked with _Host.cshtml for server-side rendering, where traditional ASP.NET Core Tag Helpers could target the <head> element directly. The new .NET 8 Blazor Web App template uses App.razor as the root component and introduces the <HeadOutlet> component for managing head content.

Approach 1: Adapting Tag Helpers

If you’re migrating existing Tag Helpers or creating new ones for head content injection, you’ll need to modify them to target HeadOutlet instead of the head element:


using Microsoft.AspNetCore.Razor.TagHelpers;

namespace YourNamespace
{
    [HtmlTargetElement("HeadOutlet")]
    public class CustomScriptTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.PostContent.AppendHtml(
                "<script src=\"_content/YourLibrary/js/script.js\"></script>"
            );
        }
    }
}
    

Remember to register your Tag Helper in _Imports.razor:

@addTagHelper *, YourLibrary

Approach 2: Using Blazor Components (Recommended)

While adapting Tag Helpers works, Blazor offers a more idiomatic approach using components and the HeadContent component. This approach aligns better with Blazor’s component-based architecture:


@namespace YourNamespace
@implements IComponentRenderMode

<HeadContent>
    <script src="_content/YourLibrary/js/script.js"></script>
</HeadContent>
    

To use this component in your App.razor:


<head>
    <!-- Other head elements -->
    <HeadOutlet @rendermode="RenderModeForPage" />
    <YourScriptComponent @rendermode="RenderModeForPage" />
</head>
    

Benefits of the Component Approach

  • Better Integration: Components work seamlessly with Blazor’s rendering model
  • Render Mode Support: Easy to control rendering based on the current render mode (Interactive Server, WebAssembly, or Auto)
  • Dynamic Content: Can leverage Blazor’s full component lifecycle and state management
  • Type Safety: Provides compile-time checking and better tooling support

Best Practices

  • Prefer the component-based approach for new development
  • Use Tag Helpers only when migrating existing code or when you need specific ASP.NET Core pipeline integration
  • Always specify the @rendermode attribute to ensure proper rendering in different scenarios
  • Place custom head content components after HeadOutlet to ensure proper ordering

Conclusion

While both approaches work in .NET 8 Blazor Web Apps, the component-based approach using HeadContent provides a more natural fit with Blazor’s architecture and offers better maintainability and flexibility. When building new applications, consider using components unless you have a specific need for Tag Helper functionality.

Integrating DevExpress Chat Component with Semantic Kernel: A Step-by-Step Guide

Integrating DevExpress Chat Component with Semantic Kernel: A Step-by-Step Guide

Are you excited to bring powerful AI chat completions to your web application? I sure am! In this post, we’ll walk through how to integrate the DevExpress Chat component with the Semantic Kernel using OpenAI. This combination can make your app more interactive and intelligent, and it’s surprisingly simple to set up. Let’s dive in!

Step 1: Adding NuGet Packages

First, let’s ensure we have all the necessary packages. Open your DevExpress.AI.Samples.Blazor.csproj file and add the following NuGet references:

 <ItemGroup>
<PackageReference Include="Microsoft.KernelMemory.Abstractions" Version="0.78.241007.1" />
<PackageReference Include="Microsoft.KernelMemory.Core" Version="0.78.241007.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.21.1" />
</ItemGroup>

 

This will bring in the core components of Semantic Kernel to power your chat completions.

Step 2: Setting Up Your Kernel in Program.cs

Next, we’ll configure the Semantic Kernel and OpenAI integration. Add the following code in your Program.cs to create the kernel and set up the chat completion service:


    //Create your OpenAI client
    string OpenAiKey = Environment.GetEnvironmentVariable("OpenAiTestKey");
    var client = new OpenAIClient(new System.ClientModel.ApiKeyCredential(OpenAiKey));

    //Adding semantic kernel
    var KernelBuilder = Kernel.CreateBuilder();
    KernelBuilder.AddOpenAIChatCompletion("gpt-4o", client);
    var sk = KernelBuilder.Build();
    var ChatService = sk.GetRequiredService<IChatCompletionService>();
    builder.Services.AddSingleton<IChatCompletionService>(ChatService);
    

This step is crucial because it connects your app to OpenAI via the Semantic Kernel and sets up the chat completion service that will drive the AI responses in your chat.

Step 3: Creating the Chat Component

Now that we’ve got our services ready, it’s time to set up the chat component. We’ll define the chat interface in our Razor page. Here’s how you can do that:

Razor Section:


    @page "/sk"
    @using DevExpress.AIIntegration.Blazor.Chat
    @using AIIntegration.Services.Chat;
    @using Microsoft.SemanticKernel.ChatCompletion
    @using System.Diagnostics
    @using System.Text.Json
    @using System.Text

    

    @inject IChatCompletionService chatCompletionsService;
    @inject IJSRuntime JSRuntime;
    

This UI will render a clean chat interface using DevExpress’s DxAIChat component, which is connected to our Semantic Kernel chat completion service.

Code Section:

Now, let’s handle the interaction logic. Here’s the code that powers the chat backend:


    @code {

        ChatHistory ChatHistory = new ChatHistory();

        async Task MessageSent(MessageSentEventArgs args)
        {
            // Add the user's message to the chat history
            ChatHistory.AddUserMessage(args.Content);

            // Get a response from the chat completion service
            var Result = await chatCompletionsService.GetChatMessageContentAsync(ChatHistory);

            // Extract the response content
            string MessageContent = Result.InnerContent.ToString();
            Debug.WriteLine("Message from chat completion service:" + MessageContent);

            // Add the assistant's message to the history
            ChatHistory.AddAssistantMessage(MessageContent);

            // Send the response to the UI
            var message = new Message(MessageRole.Assistant, MessageContent);
            args.SendMessage(message);
        }
    }
    

With this in place, every time the user sends a message, the chat completion service will process the conversation history and generate a response from OpenAI. The result is then displayed in the chat window.

Step 4: Run Your Project

Before running the project, ensure that the correct environment variable for the OpenAI key is set (OpenAiTestKey). This key is necessary for the integration to communicate with OpenAI’s API.

Now, you’re ready to test! Simply run your project and navigate to https://localhost:58108/sk. Voilà! You’ll see a beautiful, AI-powered chat interface waiting for your input. 🎉

Conclusion

And that’s it! You’ve successfully integrated the DevExpress Chat component with the Semantic Kernel for AI-powered chat completions. Now, you can take your user interaction to the next level with intelligent, context-aware responses. The possibilities are endless with this integration—whether you’re building a customer support chatbot, a productivity assistant, or something entirely new.

Let me know how your integration goes, and feel free to share what cool things you build with this!

here is the full implementation GitHub