openui-forge-csharp

A starter setup for building generative user interfaces with a React frontend and a C# ASP.NET Core backend. It sends OpenAI responses to the browser as server-sent events, which are updates delivered while the response is being created.

In plain words
What is it for?
Use it to create OpenUI apps with ASP.NET Core Minimal APIs, connect React components to OpenAI responses, and stream generated interface content.
Why use it?
It removes the need to build the React-to-.NET streaming connection and basic backend endpoint yourself. It also documents the required .NET version and OpenAI key.

Skill for Claude CodeCodex

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add skills/othmanadi/openui-forge/openui-forge-csharp
Any agent
npx skills add OthmanAdi/openui-forge --skill openui-forge-csharp
Clone the repo
git clone --depth 1 https://github.com/OthmanAdi/openui-forge

Made for: Claude Code, Codex.

Per session 37 Skills are progressive disclosure: only the name and description are preloaded; the body loads when the skill is used.
When invoked 2,726 The whole file, excluding the scripts and references it only reads on demand.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.00037 $0.02726
Opus 5 $0.00018 $0.01363
Sonnet 5 $0.00007 $0.00545
Haiku 4.5 $0.00004 $0.00273

Measured 2d ago against content hash 0619cf8acc73, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

openui-forge-csharp scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

.agents/skills/openui-forge-csharp/SKILL.md · 271 lines

How it starts

The opening of the file, as written. The whole thing — 271 lines — stays where its author put it; the contents beside it link to each section on GitHub.

OpenUI Forge — C#

Build generative UI apps with a React frontend + C# backend. Streams OpenAI API responses directly via an ASP.NET Core Minimal API (.NET 10 LTS) using HttpClient.

Activation Triggers

  • "openui csharp", "openui c#", "openui dotnet", "openui aspnet"
  • "generative ui csharp", "c# streaming ui backend", "asp.net core openui"

Prerequisites

  • Node.js >= 22 (24 LTS recommended) + React >= 18.3.1 (19+ recommended) (frontend)
  • .NET SDK 10.0 (backend; .NET 10 is the current LTS, supported until Nov 2028. .NET 8 LTS also works but reaches end of support Nov 2026.)
  • OPENAI_API_KEY environment variable set

Quick Start

  1. Create the React frontend and install OpenUI deps:
npm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
  1. Generate the system prompt:
npx @openuidev/cli generate ./src/lib/library.ts --out backend/system-prompt.txt
  1. Create the C# backend (see Full Code below)
  2. Run: dotnet run on :5000, frontend on :3000

Full Code

Backend: backend/openui-backend.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

No NuGet packages required. ASP.NET Core, HttpClient/IHttpClientFactory, and System.Text.Json all ship in the .NET 10 shared framework referenced by Microsoft.NET.Sdk.Web.

Backend: backend/Program.cs

using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);

// Pooled, correctly-disposed HttpClient instances. Infinite timeout because
// this is a long-lived streaming proxy; client disconnects cancel the request.
builder.Services.AddHttpClient("openai", client =>
{
    client.Timeout = Timeout.InfiniteTimeSpan;
});

// CORS: lock to the configured frontend origin. Do NOT use AllowAnyOrigin —
// a wildcard would let any site call this backend and burn your API key.
var frontendOrigin =
    Environment.GetEnvironmentVariable("FRONTEND_ORIGIN") ?? "http://localhost:3000";

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
        policy.WithOrigins(frontendOrigin)
              .WithMethods("POST", "OPTIONS")
              .AllowAnyHeader());
});

var app = builder.Build();

app.UseCors();

// Load the generated system prompt ONCE at startup; fail fast if missing.
var promptPath = Path.Combine(Directory.GetCurrentDirectory(), "system-prompt.txt");
if (!File.Exists(promptPath))
{
    throw new FileNotFoundException(
        "system-prompt.txt not found. Generate it with: " +
        "npx @openuidev/cli generate ./src/lib/library.ts --out system-prompt.txt",
        promptPath);
}
var systemPrompt = await File.ReadAllTextAsync(promptPath);

var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
var baseUrl = (Environment.GetEnvironmentVariable("OPENAI_BASE_URL") ?? "https://api.openai.com/v1")
    .TrimEnd('/');
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-5.5";

app.MapPost("/api/chat", async (ChatRequest req, IHttpClientFactory httpClientFactory, HttpContext ctx) =>
{
    if (string.IsNullOrEmpty(apiKey))
        return Results.Json(new { error = "OPENAI_API_KEY not set" }, statusCode: 500);
    if (req.Messages is null || req.Messages.Length == 0)
        return Results.Json(new { error = "messages must be a non-empty array" }, statusCode: 400);

    // Prepend the server-side system prompt; never trust the client to supply it.
    var messages = new List<ChatMessage> { new("system", systemPrompt) };
    messages.AddRange(req.Messages);

    var payload = JsonSerializer.Serialize(new OpenAiRequest(model, true, messages));

    using var upstreamRequest = new HttpRequestMessage(
        HttpMethod.Post, $"{baseUrl}/chat/completions")
    {
        Content = new StringContent(payload, Encoding.UTF8, "application/json"),
    };
    upstreamRequest.Headers.Add("Authorization", $"Bearer {apiKey}");

    var client = httpClientFactory.CreateClient("openai");

    // ResponseHeadersRead returns as soon as headers arrive, so we read the
    // body incrementally off the socket instead of buffering it into memory.
    var upstream = await client.SendAsync(
        upstreamRequest, HttpCompletionOption.ResponseHeadersRead, ctx.RequestAborted);

    if (!upstream.IsSuccessStatusCode)
    {
        var errorBody = await upstream.Content.ReadAsStringAsync(ctx.RequestAborted);
        upstream.Dispose();
        return Results.Json(
            new { error = $"OpenAI returned {(int)upstream.StatusCode}: {errorBody}" },
            statusCode: (int)upstream.StatusCode);
    }

    return Results.Extensions.SseProxy(upstream);
});

app.Run();

// SSE passthrough: forward OpenAI's native `data: {chunk}\n\n` lines verbatim,
// flushing after each so tokens appear as they arrive. Pair with openAIAdapter().
//
// Idiomatic .NET 10 alternative: if you parse each chunk into a typed payload,
// return TypedResults.ServerSentEvents(IAsyncEnumerable<SseItem<T>>) from
// System.Net.ServerSentEvents — the framework writes the SSE framing for you.
// We keep the raw passthrough because the upstream is ALREADY valid SSE.
static class ResultExtensions
{
    public static IResult SseProxy(this IResultExtensions _, HttpResponseMessage upstream)
        => new SseProxyResult(upstream);
}

sealed class SseProxyResult(HttpResponseMessage upstream) : IResult
{
    public async Task ExecuteAsync(HttpContext httpContext)
    {
        var response = httpContext.Response;
        response.StatusCode = 200;
        response.ContentType = "text/event-stream";
        response.Headers.CacheControl = "no-cache";
        response.Headers.Connection = "keep-alive";
        response.Headers["X-Accel-Buffering"] = "no"; // defeat proxy buffering

        try
        {
            await using var upstreamStream =
                await upstream.Content.ReadAsStreamAsync(httpContext.RequestAborted);
            using var reader = new StreamReader(upstreamStream, Encoding.UTF8);

            while (await reader.ReadLineAsync(httpContext.RequestAborted) is { } line)
            {
                if (line.Length == 0) continue; // re-emit our own framing below

                await response.WriteAsync(line + "\n", httpContext.RequestAborted);
                if (line.StartsWith("data:", StringComparison.Ordinal))
                    await response.WriteAsync("\n", httpContext.RequestAborted);
                await response.Body.FlushAsync(httpContext.RequestAborted);

                if (line == "data: [DONE]") break;
            }
        }
        catch (OperationCanceledException)
        {
            // Client aborted — nothing to do.
        }
        finally
        {
            upstream.Dispose();
        }
    }
}

// JsonPropertyName pins wire names to lowercase: incoming binding is
// case-insensitive, but Serialize defaults to PascalCase and OpenAI needs
// lowercase role/content/model.
record ChatMessage(
    [property: JsonPropertyName("role")] string Role,
    [property: JsonPropertyName("content")] string Content);

record ChatRequest(
    [property: JsonPropertyName("messages")] ChatMessage[] Messages);

record OpenAiRequest(
    [property: JsonPropertyName("model")] string Model,
    [property: JsonPropertyName("stream")] bool Stream,
    [property: JsonPropertyName("messages")] List<ChatMessage> Messages);

Read the full file on GitHub · 271 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 2d ago First seen · 271 lines · 37 tokens per session scan A 0619cf8acc73

Subscribe to this mod's changes

openui-forge-csharp is a skill published in the GitHub repository OthmanAdi/openui-forge (22 stars, last pushed 29d ago), licensed MIT. It adds 37 tokens to every session and 2,726 once invoked, about $0.0002 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.