The DLL Registration Trap in Legacy .NET Framework Applications

The DLL Registration Trap in Legacy .NET Framework Applications

If you’ve ever worked on a traditional .NET Framework application — the kind that predates .NET Core and .NET 5+ — this story may feel painfully familiar.

I’m talking about classic .NET Framework 4.x applications (4.0, 4.5, 4.5.1, 4.5.2, 4.6, 4.6.1, 4.6.2, 4.7, 4.7.1, 4.7.2, 4.8, and the final release 4.8.1). These systems often live long, productive lives… and accumulate interesting technical debt along the way.

This particular system is written in C# and relies heavily on COM components to render video, audio, and PDF content. Under the hood, many of these components are based on technologies like DirectShow filters, ActiveX controls, or other native COM DLLs.

And that’s where the story begins.


The Setup: COM, DirectShow, and Registration

Unlike managed .NET assemblies, COM components don’t just live quietly next to your executable. They need to be registered in the system registry so Windows knows:

  • What CLSID they expose
  • Which DLL implements that CLSID
  • Whether it’s 32-bit or 64-bit
  • How it should be activated

For DirectShow-based components (very common for video/audio playback in legacy apps), registration is usually done manually during development using regsvr32.

Example:

regsvr32 MyVideoFilter.dll

To unregister:

regsvr32 /u MyVideoFilter.dll

Important detail that bites a lot of people:

  • 32-bit DLLs must be registered using:
C:\Windows\SysWOW64\regsvr32.exe My32BitFilter.dll
  • 64-bit DLLs must be registered using:
C:\Windows\System32\regsvr32.exe My64BitFilter.dll

Yes — the folder names are historically confusing.


Development Works… Until It Doesn’t

So here’s the usual development flow:

  1. You register all required COM DLLs on your development machine
  2. Visual Studio runs the app
  3. Video plays, audio works, PDFs render
  4. Everyone is happy

Then comes the next step.

“Let’s build an installer.”


The Installer Paradox

This is where the real battle story begins.

Your application installer (MSI, InstallShield, WiX, Inno Setup — pick your poison) now needs to:

  • Copy the COM DLLs
  • Register them during installation
  • Unregister them during uninstall

This seems reasonable… until you test it.

The Loop From Hell

Here’s what happens in practice:

  • You install your app for testing
  • The installer registers its own copies of the COM DLLs
  • Your development environment was using different copies (maybe newer, maybe local builds)
  • Suddenly:
    • Your source build stops working
    • Visual Studio debugging breaks
    • Another app on your machine mysteriously fails

Then you:

  • Uninstall the app
  • The installer unregisters the DLLs
  • Now nothing works anymore

So you re-register the DLLs manually for development…

…and the cycle repeats.


The Battle Story: It Only Worked… Until It Didn’t

For a long time, this system appeared to work just fine.

Video played. Audio rendered. PDFs opened. No obvious errors.

What we didn’t realize at first was a dangerous hidden assumption:

The system only worked on machines where a previous version had already been installed.

Those older installations had left COM DLLs registered in the system — quietly, globally, and invisibly.

So when we deployed a new version without removing the old one:

  • Everything looked fine
  • No one suspected missing registrations
  • The system passed casual testing

The illusion broke the moment we tried a clean installation.

On a fresh machine — no previous version, no leftover registry entries — the application suddenly failed:

  • Components didn’t initialize
  • Media rendering silently broke
  • COM activation errors appeared only in Event Viewer

The installer claimed it was registering the DLLs.

In reality, it wasn’t doing it correctly — or at least not in the way the application actually needed.

That’s when we realized we were standing on years of accidental state.


Why This Happens

The core problem is simple but brutal:

COM registration is global and mutable.

There is:

  • One registry
  • One CLSID mapping
  • One “active” DLL per COM component

Your development environment, your installed application, and your installer are all fighting over the same global state.

.NET Framework itself isn’t the villain here — it’s just sitting on top of an old Windows integration model that predates modern isolation concepts.


A New Player Enters: ARM64

Just when we thought the problem space was limited to x86 vs x64, another variable entered the scene.

One of the development machines was ARM64.

Modern Windows on ARM adds a new layer of complexity:

  • ARM64 native processes
  • x64 emulation
  • x86 emulation on top of ARM64

From the outside, everything looks like it’s running on x64.

Under the hood, it’s not that simple.

Why This Makes COM Registration Worse

COM registration is architecture-specific:

  • x86 DLLs register under one view of the registry
  • x64 DLLs register under another
  • ARM64 introduces yet another execution context

On Windows ARM:

  • System32 contains ARM64 binaries
  • SysWOW64 contains x86 binaries
  • x64 binaries often run through emulation layers

So now the questions multiply:

  • Which regsvr32 did the installer call?
  • Was it ARM64, x64, or x86?
  • Did the app run natively, or under emulation?
  • Did the COM DLL match the process architecture?

The result is a system where:

  • Some things work on Intel machines
  • Some things work on ARM machines
  • Some things only work if another version was installed first

At this point, debugging stops being logical and starts being archaeological.


Why This Is So Common in .NET Framework 4.x Apps

Many enterprise and media-heavy applications built on:

  • .NET Framework 4.0–4.8.1
  • WinForms or WPF
  • DirectShow or ActiveX components

were designed in an era where:

  • Global COM registration was normal
  • Side-by-side isolation was rare
  • “Just register the DLL” was accepted practice

These systems work, but they’re fragile — especially on developer machines.


Where the Article Is Going Next

In the rest of this article series, we’ll look at:

  • Why install-time registration is often a mistake
  • How to isolate development vs runtime environments
  • Techniques like:
    • Dedicated dev VMs
    • Registration-free COM (where possible)
    • App-local COM deployment
    • Clear ownership rules for installers
  • How to survive (and maintain) legacy .NET Framework systems without losing your sanity

If you’ve ever broken your own development environment just by testing your installer — you’re not alone.

This is the cost of living at the intersection of managed code and unmanaged history.

ConfigureAwait(false): Why It Exists, What It Solves, and When Context Is the Real Bug

ConfigureAwait(false): Why It Exists, What It Solves, and When Context Is the Real Bug

Async/await in C# is often described as “non-blocking,” but that description hides an important detail:

await is not just about waiting — it is about where execution continues afterward.

Understanding that single idea explains:

  • why deadlocks happen,
  • why ConfigureAwait(false) exists,
  • and why it *reduces* damage without fixing the root cause.

This article is not just theory. It’s written because this exact class of problem showed up again in real production code during the first week of 2026 — and it took a context-level fix to resolve it.

The Hidden Mechanism: Context Capture

When you await a task, C# does two things:

  1. It pauses the current method until the awaited task completes.
  2. It captures the current execution context (if one exists) so the continuation can resume there.

That context might be:

  • a UI thread (WPF, WinForms, MAUI),
  • a request context (classic ASP.NET),
  • or no special context at all (ASP.NET Core, console apps).

This default behavior is intentional. It allows code like this to work safely:

var data = await LoadAsync();
MyLabel.Text = data.Name; // UI-safe continuation

But that same mechanism becomes dangerous when async code is blocked synchronously.

The Root Problem: Blocking on Async

Deadlocks typically appear when async code is forced into a synchronous shape:

var result = GetDataAsync().Result; // or .Wait()

What happens next:

  1. The calling thread blocks, waiting for the async method to finish.
  2. The async method completes its awaited operation.
  3. The continuation tries to resume on the original context.
  4. That context is blocked.
  5. Nothing can proceed.

💥 Deadlock.

This is not an async bug. This is a context dependency cycle.

The Blast Radius Concept

Blocking on async is the explosion.

The blast radius is how much of the system is taken down with it.

Full blast (default await)

  • Continuation *requires* the blocked context
  • The async operation cannot complete
  • The caller never unblocks
  • Everything stops

Reduced blast (ConfigureAwait(false))

  • Continuation does not require the original context
  • It resumes on a thread pool thread
  • The async operation completes
  • The blocking call unblocks

The original mistake still exists — but the damage is contained.

The real fix is “don’t block on async,”
but ConfigureAwait(false) reduces the blast radius when someone does.

What ConfigureAwait(false) Actually Does

await SomeAsyncOperation().ConfigureAwait(false);

This tells the runtime:

“I don’t need to resume on the captured context. Continue wherever it’s safe to do so.”

Important clarifications:

  • It does not make code faster by default
  • It does not make code parallel
  • It does not remove the need for proper async flow
  • It only removes context dependency

Why This Matters in Real Code

Async code rarely exists in isolation.

A method often awaits another method, which awaits another:

await AAsync();
    await BAsync();
        await CAsync();

If any method in that chain requires a specific context, the entire chain becomes context-bound.

That is why:

  • library code must be careful,
  • deep infrastructure layers must avoid context assumptions,
  • and UI layers must be explicit about where context is required.

When ConfigureAwait(false) Is the Right Tool

Use it when all of the following are true:

  • The method does not interact with UI state
  • The method does not depend on a request context
  • The method is infrastructure, library, or backend logic
  • The continuation does not care which thread resumes it

This is especially true for:

  • NuGet packages
  • shared libraries
  • data access layers
  • network and IO pipelines

What It Is Not

ConfigureAwait(false) is not:

  • a fix for bad async usage
  • a substitute for proper async flow
  • a reason to block on tasks
  • something to blindly apply everywhere

It is a damage-control tool, not a cure.

A Real Incident: When None of the Usual Fixes Worked

First week of 2026.

The first task I had with the programmers in my office was to investigate a problem in a trading block. The symptoms looked like a classic async issue: timing bugs, inconsistent behavior, and freezes that felt “await-shaped.”

We did what experienced .NET teams typically do when async gets weird:

  • Reviewed the full async/await chain end-to-end
  • Double-checked the source code carefully (everything looked fine)
  • Tried the usual “tools people reach for” under pressure:
  • .Wait()
  • .GetAwaiter().GetResult()
  • wrapping in Task.Run(...)
  • adding ConfigureAwait(false)
  • mixing combinations of those approaches

None of it reliably fixed the problem.

At that point it stopped being a “missing await” story. It became a “the model is right but reality disagrees” story.

One of the programmers, Daniel, and I went deeper. I found myself mentally replaying every async pattern I know — especially because I’ve written async-heavy code myself, including library work like SyncFramework, where I synchronize databases and deal with long-running operations.

That’s the moment where this mental model matters: it forces you to stop treating await like syntax and start treating it like mechanics.

The Actual Root Cause: It Was the Context

In the end, the culprit wasn’t which pattern we used — it was where the continuation was allowed to run.

This application was built on DevExpress XAF. In this environment, the “correct” continuation behavior is often tied to XAF’s own scheduling and application lifecycle rules. XAF provides a mechanism to run code in its synchronization context — for example using BlazorApplication.InvokeAsync, which ensures that continuations run where the framework expects.

Once we executed the problematic pipeline through XAF’s synchronization context, the issue was solved.

No clever pattern. No magical await. No extra parallelism.

Just: the right context.

And this is not unique to XAF. Similar ideas exist in:

  • Windows Forms (UI thread affinity + SynchronizationContext)
  • WPF (Dispatcher context)
  • Any framework that requires work to resume on a specific thread/context

Why I’m Writing This

What I wanted from this experience is simple: don’t forget it.

Because what makes this kind of incident dangerous is that it looks like a normal async bug — and the internet is full of “four fixes” people cycle through:

  1. add/restore missing await
  2. use .Wait() / .Result
  3. wrap in Task.Run()
  4. use ConfigureAwait(false)

Sometimes those are relevant. Sometimes they’re harmful. And sometimes… they’re all beside the point.

In our case, the missing piece was framework context — and once you see that, you realize why the “blast radius” framing is so useful:

  • Blocking is the explosion.
  • ConfigureAwait(false) contains damage when someone blocks.
  • If a framework requires a specific synchronization context, the fix may be to supply the correct context explicitly.

That’s what happened here. And that’s why I’m capturing it as live knowledge, not just documentation.

The Mental Model to Keep

  • Async bugs are often context bugs
  • Blocking creates the explosion
  • Context capture determines the blast radius
  • ConfigureAwait(false) limits the damage
  • Proper async flow prevents the explosion entirely
  • Frameworks may require their own synchronization context
  • Correct async code can still fail in the wrong context

Async is not just about tasks. It’s about where your code is allowed to continue.

 

Guide to Blazor Component Design and Implementation for backend devs

Guide to Blazor Component Design and Implementation for backend devs

Over time, I transitioned to using the first versions of my beloved framework, XAF. As you might know, XAF generates a polished and functional UI out of the box. Using XAF made me more of a backend developer since most of the development work wasn’t visual—especially in the early versions, where the model designer was rudimentary (it’s much better now).

Eventually, I moved on to developing .NET libraries and NuGet packages, diving deep into SOLID design principles. Fun fact: I actually learned about SOLID from DevExpress TV. Yes, there was a time before YouTube when DevExpress posted videos on technical tasks!

Nowadays, I feel confident creating and publishing my own libraries as NuGet packages. However, my “old monster” was still lurking in the shadows: UI components. I finally decided it was time to conquer it, but first, I needed to choose a platform. Here were my options:

  1. Windows Forms: A robust and mature platform but limited to desktop applications.
  2. WPF: A great option with some excellent UI frameworks that I love, but it still feels a bit “Windows Forms-ish” to me.
  3. Xamarin/Maui: I’m a big fan of Xamarin Forms and Xamarin/Maui XAML, but they’re primarily focused on device-specific applications.
  4. Blazor: This was the clear winner because it allows me to create desktop applications using Electron, embed components into Windows Forms, or even integrate with MAUI.

Recently, I’ve been helping my brother with a project in Blazor. (He’s not a programmer, but I am.) This gave me an opportunity to experiment with design patterns to get the most out of my components, which started as plain HTML5 pages.

Without further ado, here are the key insights I’ve gained so far.

Building high-quality Blazor components requires attention to both the C# implementation and Razor markup patterns. This guide combines architectural best practices with practical implementation patterns to create robust, reusable components.

1. Component Architecture and Organization

Parameter Organization

Start by organizing parameters into logical groups for better maintainability:

public class CustomForm : ComponentBase
{
    // Layout Parameters
    [Parameter] public string Width { get; set; }
    [Parameter] public string Margin { get; set; }
    [Parameter] public string Padding { get; set; }
    
    // Validation Parameters
    [Parameter] public bool EnableValidation { get; set; }
    [Parameter] public string ValidationMessage { get; set; }
    
    // Event Callbacks
    [Parameter] public EventCallback<bool> OnValidationComplete { get; set; }
    [Parameter] public EventCallback<string> OnSubmit { get; set; }
}

Corresponding Razor Template

<div class="form-container" style="width: @Width; margin: @Margin; padding: @Padding">
    <form @onsubmit="HandleSubmit">
        @if (EnableValidation)
        {
            <div class="validation-message">
                @ValidationMessage
            </div>
        }
        @ChildContent
    </form>
</div>

2. Smart Default Values and Template Composition

Component Implementation

public class DataTable<T> : ComponentBase
{
    [Parameter] public int PageSize { get; set; } = 10;
    [Parameter] public bool ShowPagination { get; set; } = true;
    [Parameter] public string EmptyMessage { get; set; } = "No data available";
    [Parameter] public IEnumerable<T> Items { get; set; } = Array.Empty<T>();
    [Parameter] public RenderFragment HeaderTemplate { get; set; }
    [Parameter] public RenderFragment<T> RowTemplate { get; set; }
    [Parameter] public RenderFragment FooterTemplate { get; set; }
}

Razor Implementation

<div class="table-container">
    @if (HeaderTemplate != null)
    {
        <header class="table-header">
            @HeaderTemplate
        </header>
    }
    
    <div class="table-content">
        @if (!Items.Any())
        {
            <div class="empty-state">@EmptyMessage</div>
        }
        else
        {
            @foreach (var item in Items)
            {
                @RowTemplate(item)
            }
        }
    </div>
    
    @if (ShowPagination)
    {
        <div class="pagination">
            <!-- Pagination implementation -->
        </div>
    }
</div>

3. Accessibility and Unique IDs

Component Implementation

public class FormField : ComponentBase
{
    private string fieldId = $"field-{Guid.NewGuid():N}";
    private string labelId = $"label-{Guid.NewGuid():N}";
    private string errorId = $"error-{Guid.NewGuid():N}";
    
    [Parameter] public string Label { get; set; }
    [Parameter] public string Error { get; set; }
    [Parameter] public bool Required { get; set; }
}

Razor Implementation

<div class="form-field">
    <label id="@labelId" for="@fieldId">
        @Label
        @if (Required)
        {
            <span class="required" aria-label="required">*</span>
        }
    </label>
    
    <input id="@fieldId" 
           aria-labelledby="@labelId"
           aria-describedby="@errorId"
           aria-required="@Required" />
    
    @if (!string.IsNullOrEmpty(Error))
    {
        <div id="@errorId" class="error-message" role="alert">
            @Error
        </div>
    }
</div>

4. Virtualization and Performance

Component Implementation

public class VirtualizedList<T> : ComponentBase
{
    [Parameter] public IEnumerable<T> Items { get; set; }
    [Parameter] public RenderFragment<T> ItemTemplate { get; set; }
    [Parameter] public int ItemHeight { get; set; } = 50;
    [Parameter] public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<T>>> ItemsProvider { get; set; }
}

Razor Implementation

<div class="virtualized-container" style="height: 500px; overflow-y: auto;">
    <Virtualize Items="@Items"
                ItemSize="@ItemHeight"
                ItemsProvider="@ItemsProvider"
                Context="item">
        <ItemContent>
            <div class="list-item" style="height: @(ItemHeight)px">
                @ItemTemplate(item)
            </div>
        </ItemContent>
        <Placeholder>
            <div class="loading-placeholder" style="height: @(ItemHeight)px">
                <div class="loading-animation"></div>
            </div>
        </Placeholder>
    </Virtualize>
</div>

Best Practices Summary

1. Parameter Organization

  • Group related parameters with clear comments
  • Provide meaningful default values
  • Use parameter validation where appropriate

2. Template Composition

  • Use RenderFragment for customizable sections
  • Provide default templates when needed
  • Enable granular control over component appearance

3. Accessibility

  • Generate unique IDs for form elements
  • Include proper ARIA attributes
  • Support keyboard navigation

4. Performance

  • Implement virtualization for large datasets
  • Use loading states and placeholders
  • Optimize rendering with appropriate conditions

Conclusion

Building effective Blazor components requires attention to both the C# implementation and Razor markup. By following these patterns and practices, you can create components that are:

  • Highly reusable
  • Performant
  • Accessible
  • Easy to maintain
  • Flexible for different use cases

Remember to adapt these practices to your specific needs while maintaining clean component design principles.

The New Era of Smart Editors: Creating a RAG system using XAF and the new Blazor chat component

The New Era of Smart Editors: Creating a RAG system using XAF and the new Blazor chat component

The New Era of Smart Editors: Developer Express and AI Integration

The new era of smart editors is already here. Developer Express has introduced AI functionality in many of their controls for .NET (Windows Forms, Blazor, WPF, MAUI).

This advancement will eventually come to XAF, but in the meantime, here at XARI, we are experimenting with XAF integrations to add value to our customers.

In this article, we are going to integrate the new chat component into an XAF application, and our first use case will be RAG (Retrieval-Augmented Generation). RAG is a system that combines external data sources with AI-generated responses, improving accuracy and relevance in answers by retrieving information from a document set or knowledge base and using it in conjunction with AI predictions.

To achieve this integration, we will follow the steps outlined in this tutorial:

Implement a Property Editor Based on Custom Components (Blazor)

Implementing the Property Editor

When I implement my own property editor, I usually avoid doing so for primitive types because, in most cases, my property editor will need more information than a simple primitive value. For this implementation, I want to handle a custom value in my property editor. I typically create an interface to represent the type, ensuring compatibility with both XPO and EF Core.

namespace XafSmartEditors.Razor.RagChat
{
    public interface IRagData
    {
        Stream FileContent { get; set; }
        string Prompt { get; set; }
        string FileName { get; set; }
    }
}

Non-Persistent Implementation

After defining the type for my editor, I need to create a non-persistent implementation:

namespace XafSmartEditors.Razor.RagChat
{
    [DomainComponent]
    public class IRagDataImp : IRagData, IXafEntityObject, INotifyPropertyChanged
    {
        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public IRagDataImp()
        {
            Oid = Guid.NewGuid();
        }

        [DevExpress.ExpressApp.Data.Key]
        [Browsable(false)]  
        public Guid Oid { get; set; }

        private string prompt;
        private string fileName;
        private Stream fileContent;

        public Stream FileContent
        {
            get => fileContent;
            set
            {
                if (fileContent == value) return;
                fileContent = value;
                OnPropertyChanged();
            }
        }

        public string FileName
        {
            get => fileName;
            set
            {
                if (fileName == value) return;
                fileName = value;
                OnPropertyChanged();
            }
        }
        
        public string Prompt
        {
            get => prompt;
            set
            {
                if (prompt == value) return;
                prompt = value;
                OnPropertyChanged();
            }
        }

        // IXafEntityObject members
        void IXafEntityObject.OnCreated() { }
        void IXafEntityObject.OnLoaded() { }
        void IXafEntityObject.OnSaving() { }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Creating the Blazor Chat Component

Now, it’s time to create our Blazor component and add the new DevExpress chat component for Blazor:

<DxAIChat CssClass="my-chat" Initialized="Initialized" 
          RenderMode="AnswerRenderMode.Markdown" 
          UseStreaming="true"
          SizeMode="SizeMode.Medium">
    <EmptyMessageAreaTemplate>
        <div class="my-chat-ui-description">
            <span style="font-weight: bold; color: #008000;">Rag Chat</span> Assistant is ready to answer your questions.
        </div>
    </EmptyMessageAreaTemplate>
    <MessageContentTemplate>
        <div class="my-chat-content">
            @ToHtml(context.Content)
        </div>
    </MessageContentTemplate>
</DxAIChat>

@code {
    IRagData _value;
    [Parameter]
    public IRagData Value
    {
        get => _value;
        set => _value = value;
    }
    
    async Task Initialized(IAIChat chat)
    {
        await chat.UseAssistantAsync(new OpenAIAssistantOptions(
            this.Value.FileName,
            this.Value.FileContent,
            this.Value.Prompt
        ));
    }

    MarkupString ToHtml(string text)
    {
        return (MarkupString)Markdown.ToHtml(text);
    }
}

The main takeaway from this component is that it receives a parameter named Value of type IRagData, and we use this value to initialize the IAIChat service in the Initialized method.

Creating the Component Model

With the interface and domain component in place, we can now create the component model to communicate the value of our domain object with the Blazor component:

namespace XafSmartEditors.Razor.RagChat
{
    public class RagDataComponentModel : ComponentModelBase
    {
        public IRagData Value
        {
            get => GetPropertyValue<IRagData>();
            set => SetPropertyValue(value);
        }

        public EventCallback<IRagData> ValueChanged
        {
            get => GetPropertyValue<EventCallback<IRagData>>();
            set => SetPropertyValue(value);
        }

        public override Type ComponentType => typeof(RagChat);
    }
}

Creating the Property Editor

Finally, let’s create the property editor class that serves as a bridge between XAF and the new component:

namespace XafSmartEditors.Blazor.Server.Editors
{
    [PropertyEditor(typeof(IRagData), true)]
    public class IRagDataPropertyEditor : BlazorPropertyEditorBase, IComplexViewItem
    {
        private IObjectSpace _objectSpace;
        private XafApplication _application;

        public IRagDataPropertyEditor(Type objectType, IModelMemberViewItem model) : base(objectType, model) { }

        public void Setup(IObjectSpace objectSpace, XafApplication application)
        {
            _objectSpace = objectSpace;
            _application = application;
        }

        public override RagDataComponentModel ComponentModel => (RagDataComponentModel)base.ComponentModel;

        protected override IComponentModel CreateComponentModel()
        {
            var model = new RagDataComponentModel();

            model.ValueChanged = EventCallback.Factory.Create<IRagData>(this, value =>
            {
                model.Value = value;
                OnControlValueChanged();
                WriteValue();
            });

            return model;
        }

        protected override void ReadValueCore()
        {
            base.ReadValueCore();
            ComponentModel.Value = (IRagData)PropertyValue;
        }

        protected override object GetControlValueCore() => ComponentModel.Value;

        protected override void ApplyReadOnly()
        {
            base.ApplyReadOnly();
            ComponentModel?.SetAttribute("readonly", !AllowEdit);
        }
    }
}

Bringing It All Together

Now, let’s create a domain object that can feed the content of a file to our chat component:

namespace XafSmartEditors.Module.BusinessObjects
{
    [DefaultClassOptions]
    public class PdfFile : BaseObject
    {
        public PdfFile(Session session) : base(session) { }

        string prompt;
        string name;
        FileData file;

        public FileData File
        {
            get => file;
            set => SetPropertyValue(nameof(File), ref file, value);
        }

        public string Name
        {
            get => name;
            set => SetPropertyValue(nameof(Name), ref name, value);
        }

        public string Prompt
        {
            get => prompt;
            set => SetPropertyValue(nameof(Prompt), ref prompt, value);
        }
    }
}

Creating the Controller

We are almost done! Now, we need to create a controller with a popup action:

namespace XafSmartEditors.Module.Controllers
{
    public class OpenChatController : ViewController
    {
        Popup

WindowShowAction Chat;

        public OpenChatController()
        {
            this.TargetObjectType = typeof(PdfFile);
            Chat = new PopupWindowShowAction(this, "ChatAction", "View");
            Chat.Caption = "Chat";
            Chat.ImageName = "artificial_intelligence";
            Chat.Execute += Chat_Execute;
            Chat.CustomizePopupWindowParams += Chat_CustomizePopupWindowParams;
        }

        private void Chat_Execute(object sender, PopupWindowShowActionExecuteEventArgs e) { }

        private void Chat_CustomizePopupWindowParams(object sender, CustomizePopupWindowParamsEventArgs e)
        {
            PdfFile pdfFile = this.View.CurrentObject as PdfFile;
            var os = this.Application.CreateObjectSpace(typeof(ChatView));
            var chatView = os.CreateObject<ChatView>();

            MemoryStream memoryStream = new MemoryStream();
            pdfFile.File.SaveToStream(memoryStream);
            memoryStream.Seek(0, SeekOrigin.Begin);

            chatView.RagData = os.CreateObject<IRagDataImp>();
            chatView.RagData.FileName = pdfFile.File.FileName;
            chatView.RagData.Prompt = !string.IsNullOrEmpty(pdfFile.Prompt) ? pdfFile.Prompt : DefaultPrompt;
            chatView.RagData.FileContent = memoryStream;

            DetailView detailView = this.Application.CreateDetailView(os, chatView);
            detailView.Caption = $"Chat with Document | {pdfFile.File.FileName.Trim()}";

            e.View = detailView;
        }
    }
}

Conclusion

That’s everything we need to create a RAG system using XAF and the new DevExpress Chat component. You can find the complete source code here: GitHub Repository.

If you want to meet and discuss AI, XAF, and .NET, feel free to schedule a meeting: Schedule a Meeting.

Until next time, XAF out!