Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CrestApps.Core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<Project Path="src/Startup/CrestApps.Core.Mvc.Samples.A2AClient/CrestApps.Core.Mvc.Samples.A2AClient.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Samples.McpClient/CrestApps.Core.Mvc.Samples.McpClient.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Web/CrestApps.Core.Mvc.Web.csproj" />
<Project Path="src/Startup/CrestApps.Core.Blazor.Web/CrestApps.Core.Blazor.Web.csproj" />
</Folder>
<Folder Name="/src/Stores/">
<Project Path="src/Stores/CrestApps.Core.Data.EntityCore/CrestApps.Core.Data.EntityCore.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageVersion Include="YesSql.Provider.Sqlite" Version="5.4.7" />
<PackageVersion Include="A2A" Version="0.3.4-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.4-preview" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.6" />
<PackageVersion Include="Anthropic" Version="12.13.0" />
<PackageVersion Include="ModelContextProtocol" Version="$(ModelContextProtocolVersion)" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="$(ModelContextProtocolVersion)" />
Expand Down Expand Up @@ -61,6 +62,7 @@
<PackageVersion Include="xunit.analyzers" Version="1.27.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.runner.inproc" Version="3.2.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.52.0" />
</ItemGroup>
<ItemGroup>
<!-- Benchmark Packages -->
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ dotnet run --project .\src\Startup\CrestApps.Core.Mvc.Web\CrestApps.Core.Mvc.Web

The MVC sample is the quickest way to see the full stack working together: AI connections, deployments, agent profiles, Chat Interactions, document processing, MCP, A2A, storage, and SignalR.

There is also a **Blazor sample** that mirrors the same feature set using Blazor Server with EntityCore (EF Core + SQLite) stores:

```powershell
dotnet run --project .\src\Startup\CrestApps.Core.Blazor.Web\CrestApps.Core.Blazor.Web.csproj
```

## Fastest way to consume it

Install the smallest useful package set for your app:
Expand Down Expand Up @@ -131,6 +137,7 @@ src/
├── Startup/
│ ├── CrestApps.Core.Aspire.AppHost/
│ ├── CrestApps.Core.Mvc.Web/
│ ├── CrestApps.Core.Blazor.Web/
│ ├── CrestApps.Core.Mvc.Samples.A2AClient/
│ └── CrestApps.Core.Mvc.Samples.McpClient/
└── CrestApps.Core.Docs/
Expand Down
5 changes: 5 additions & 0 deletions aspire.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"appHost": {
"path": "src/Startup/CrestApps.Core.Aspire.AppHost/CrestApps.Core.Aspire.AppHost.csproj"
}
}
179 changes: 179 additions & 0 deletions src/CrestApps.Core.Docs/docs/core/blazor-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
---
sidebar_label: Blazor Example
sidebar_position: 21
title: Blazor Example Application
description: Complete walkthrough of the CrestApps.Core.Blazor.Web example application showing how to bootstrap a full AI-powered Blazor Server application with EntityCore stores.
---

# Blazor Example Application

> A complete walkthrough of the `CrestApps.Core.Blazor.Web` example project that demonstrates every framework feature in a Blazor Server application using Entity Framework Core (EntityCore) stores.

## Application Structure

```text
CrestApps.Core.Blazor.Web/
├── Program.cs ← Full startup configuration (EntityCore stores)
├── Components/
│ ├── App.razor ← Root Blazor component
│ ├── Routes.razor ← Router with auth
│ ├── Layout/
│ │ └── MainLayout.razor ← Admin sidebar layout
│ ├── Pages/
│ │ ├── Home/ ← Dashboard
│ │ ├── Account/ ← Login, AccessDenied
│ │ ├── Admin/ ← Articles CRUD, Settings
│ │ ├── AI/ ← Connections, Deployments, Profiles, Templates
│ │ ├── AIChat/ ← AI chat sessions, analytics
│ │ ├── ChatInteractions/ ← Interactive chat testing
│ │ ├── A2A/ ← A2A connection management
│ │ ├── Mcp/ ← MCP connections, prompts, resources
│ │ ├── DataSources/ ← Data source management
│ │ └── Indexing/ ← Index profile management
│ └── Shared/ ← Pager, AlertMessage components
├── Areas/ ← Services, models, hubs, endpoints (non-UI)
├── Controllers/ ← AccountController for cookie auth
├── Services/ ← SiteSettingsStore, BlazorAppDbContext, etc.
├── Tools/ ← Custom AI tools
├── App_Data/ ← Runtime data (EF Core SQLite DB)
└── wwwroot/ ← Static files
```

The Blazor sample mirrors the same feature areas as the MVC example but uses Razor components with `@code` blocks instead of controllers and Razor views. Non-UI concerns such as services, models, SignalR hubs, and API endpoints remain under `Areas/` and `Services/`.

## Key Differences from MVC

| Concern | MVC Example | Blazor Example |
|---------|------------|----------------|
| **Rendering** | Controllers + Razor Views | Blazor Server (`InteractiveServerRenderMode`) with Razor components |
| **Data stores** | YesSql (document store + SQLite) | EntityCore (EF Core + SQLite) |
| **Forms** | HTML forms with tag helpers | `EditForm` with `InputText`, `InputSelect`, `InputNumber` |
| **Navigation** | `RedirectToAction()` | `NavigationManager.NavigateTo()` |
| **Auth flow** | Cookie auth in MVC controllers | Cookie auth still uses `AccountController` for form POST; Blazor pages use `[Authorize]` |
| **App-specific data** | YesSql collections and indexes | `BlazorAppDbContext` for articles, analytics, and app-owned tables |
| **Real-time chat** | SignalR hubs with JS interop | SignalR client via `HubConnectionBuilder` in Blazor components |
| **Shared components** | Partial views and tag helpers | Reusable Razor components (`Pager`, `AlertMessage`, etc.) |

## Startup Configuration Walkthrough

The `Program.cs` follows the same numbered-section pattern as the MVC example, with key differences for Blazor and EntityCore.

### Section 1 — Logging

Same as the MVC example: NLog with daily log file rotation in `App_Data/logs/`.

### Section 2 — Application Configuration

Same layered configuration pattern: `appsettings.json` → `appsettings.{Environment}.json` → `App_Data/appsettings.json` as the highest-priority local override. `SiteSettingsStore` manages mutable admin settings backed by `App_Data/site-settings.json`.

### Section 3 — ASP.NET Core Blazor Setup

Instead of `AddControllersWithViews()`, the Blazor host registers:

- `AddRazorComponents().AddInteractiveServerComponents()` for Blazor Server rendering
- `AddSignalR()` with camelCase JSON payloads
- `AddControllersWithViews()` is still registered for the `AccountController` cookie-auth endpoints

### Section 4 — Authentication & Authorization

Same cookie-based authentication with an `"Admin"` authorization policy requiring the Administrator role. The `AccountController` handles login/logout form POSTs because Blazor Server cannot issue HTTP redirects with set-cookie headers directly.

### Section 5 — CrestApps Foundation + AI Services (EntityCore)

The core framework registration chain uses EntityCore stores instead of YesSql:

```csharp
builder.Services.AddCrestAppsCore(crestApps => crestApps
.AddAISuite(ai => ai
.AddEntityCoreStores()
.AddMarkdown()
.AddCopilotOrchestrator()
.AddChatInteractions(chat => chat
.AddEntityCoreStores()
.ConfigureChatHubOptions<ChatInteractionHub>()
)
.AddDocumentProcessing(documentProcessing => documentProcessing
.AddEntityCoreStores()
.AddOpenXml()
.AddPdf()
)
.AddAIMemory(memory => memory
.AddEntityCoreStores()
)
.AddA2AClient(a2a => a2a
.AddEntityCoreStores()
)
.AddMcpClient(mcp => mcp
.AddEntityCoreStores()
)
.AddMcpServer(mcpServer => mcpServer
.AddEntityCoreStores()
.AddFtpResources()
.AddSftpResources()
)
.AddSignalR(addStoreCommitterFilter: true)
.AddA2AHost()
.AddOpenAI()
.AddAzureOpenAI()
.AddOllama()
.AddAzureAIInference()
)
.AddIndexingServices(indexing => indexing
.AddEntityCoreStores()
.AddElasticsearch(/* ... */)
.AddAzureAISearch(/* ... */)
)
.AddEntityCoreSqliteDataStore("Data Source=App_Data/crestapps.db")
);
```

Every feature builder calls `.AddEntityCoreStores()` instead of `.AddYesSqlStores()`. The root builder calls `.AddEntityCoreSqliteDataStore(...)` (which maps to `AddCoreEntityCoreSqliteDataStore(...)`) instead of `.AddYesSqlDataStore(...)`.

Because EntityCore commits each write immediately via `SaveChangesAsync()`, there is no request-level unit-of-work middleware. This is one fewer piece of infrastructure compared to YesSql hosts.

### Section 6 — Schema Initialization

At startup, the Blazor host calls `InitializeEntityCoreSchemaAsync()` to apply EF Core migrations and ensure the SQLite database schema is current. This replaces the YesSql table-creation and index-registration step used in the MVC example.

### Sections 7–13

The remaining sections (providers, Elasticsearch, Azure AI Search, MCP, custom tools, background tasks, and middleware pipeline) follow the same structure as the MVC example. The middleware pipeline adds:

- `MapRazorComponents<App>().AddInteractiveServerRenderMode()` instead of MVC route patterns
- MVC controller routes are still mapped for `AccountController`
- SignalR hub endpoints remain at `/hubs/ai-chat` and `/hubs/chat-interaction`
- MCP SSE endpoint at `/mcp`

## Running the Application

**Visual Studio:** Set `CrestApps.Core.Blazor.Web` as the startup project and press **F5** (or **Ctrl+F5** to run without debugging).

**Command line:**

```bash
dotnet run --project .\src\Startup\CrestApps.Core.Blazor.Web\CrestApps.Core.Blazor.Web.csproj
```

The application starts on `https://localhost:5200`. Default login credentials are **admin** / **admin**.

Configure AI provider connections in `App_Data/appsettings.json` before using AI features.

**Aspire integration:** The Blazor sample is included in the `CrestApps.Core.Aspire.AppHost` project alongside the MVC sample, so `dotnet run` on the AppHost boots both applications together.

## Feature Parity

The Blazor example replicates all MVC example features:

- **All 19 feature areas** with full CRUD pages (AI Connections, Deployments, Profiles, Templates, Chat Interactions, A2A, MCP, Data Sources, Indexing, Articles, Settings, and more)
- **Same sidebar navigation**, UI layout, and field grouping
- **SignalR real-time chat** for AI Chat and Chat Interactions using `HubConnectionBuilder` in Blazor components
- **Analytics dashboards** with filtering and CSV export
- **Admin settings** with all 8 configuration sections (General, AI, Chat, Speech, Admin Widget, Connections, Deployments, and Data Sources)

## Key Takeaways

1. **EntityCore is a drop-in replacement for YesSql** — swap `.AddYesSqlStores()` for `.AddEntityCoreStores()` and `.AddYesSqlDataStore(...)` for `.AddEntityCoreSqliteDataStore(...)`
2. **Blazor Server works seamlessly** — the framework's DI-first design means the same services power both MVC and Blazor hosts
3. **Cookie auth bridges the gap** — `AccountController` handles login/logout POSTs while Blazor pages use `[Authorize]` and `AuthenticationStateProvider`
4. **No unit-of-work middleware needed** — EntityCore commits immediately, simplifying the middleware pipeline
5. **Same feature set, different UI model** — choose MVC or Blazor based on your team's preference; the framework layer is identical
4 changes: 4 additions & 0 deletions src/CrestApps.Core.Docs/docs/core/data-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ The repository now ships two first-party persistence flavors:

You can also implement the same interfaces with another ORM, a remote service, or any custom storage approach.

:::tip
The **[MVC Example](mvc-example.md)** demonstrates YesSql stores in a full application, while the **[Blazor Example](blazor-example.md)** (`CrestApps.Core.Blazor.Web`) demonstrates EntityCore stores. Compare both to see how each persistence flavor integrates with the framework.
:::

## Catalog Interfaces

### `ICatalog<T>`
Expand Down
17 changes: 15 additions & 2 deletions src/CrestApps.Core.Docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,21 @@ dotnet run --project .\src\Startup\CrestApps.Core.Mvc.Web\CrestApps.Core.Mvc.Web

Use the MVC sample when you want to see the full framework in one place: AI providers, deployments, profiles, templates, document processing, MCP, A2A, storage, and SignalR-driven chat flows.

### Blazor sample application

**Visual Studio:** Set `CrestApps.Core.Blazor.Web` as the startup project and press **F5** (or **Ctrl+F5** to run without debugging).

**Command line:**

```bash
dotnet run --project .\src\Startup\CrestApps.Core.Blazor.Web\CrestApps.Core.Blazor.Web.csproj
```

The Blazor sample mirrors the MVC feature set but uses Blazor Server (`InteractiveServerRenderMode`) with EntityCore (EF Core + SQLite) stores instead of YesSql. Use it when you prefer Razor components over MVC controllers and views.

### Aspire host

The Aspire host boots the MVC sample and related sample clients together as a composed local environment.
The Aspire host boots the MVC and Blazor samples and related sample clients together as a composed local environment.

:::info Prerequisites
Aspire manages containers for services like Redis. You need a container runtime such as [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running before starting the Aspire host.
Expand Down Expand Up @@ -129,7 +141,8 @@ Under the hood, each builder step still maps to the corresponding `AddCrestApps.

- Start with **[Core overview](core/index.md)** to understand the package layout
- Use **[ASP.NET Core integration](core/getting-started-aspnet.md)** to wire the same services into MVC, Razor Pages, Blazor, Minimal APIs, or MAUI hybrid hosts
- Follow **[MVC example](core/mvc-example.md)** for a complete working composition
- Follow **[MVC example](core/mvc-example.md)** for a complete working composition with YesSql
- Follow **[Blazor example](core/blazor-example.md)** for a complete working composition with EntityCore

## Build the docs site

Expand Down
3 changes: 2 additions & 1 deletion src/CrestApps.Core.Docs/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ The framework fits standard .NET dependency injection and works well in:
- **[Getting Started](getting-started.md)** for the quickest path from package install to first prompt
- **[Core Overview](core/index.md)** for the feature catalog and package layout
- **[AI Chat Use Cases](core/use-cases.md)** for real-world scenarios
- **[MVC Example](core/mvc-example.md)** for the complete reference host
- **[MVC Example](core/mvc-example.md)** for the complete reference host with YesSql
- **[Blazor Example](core/blazor-example.md)** for the complete reference host with EntityCore

1 change: 1 addition & 0 deletions src/CrestApps.Core.Docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const sidebars = {
'core/index',
'core/interfaces',
'core/mvc-example',
'core/blazor-example',
],
},
{
Expand Down
13 changes: 8 additions & 5 deletions src/Primitives/CrestApps.Core/Filters/StoreCommitterHubFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ public StoreCommitterHubFilter(ILogger<StoreCommitterHubFilter> logger)
{
var result = await next(invocationContext);

var committer = invocationContext.ServiceProvider.GetRequiredService<IStoreCommitter>();
if (_logger.IsEnabled(LogLevel.Debug))
var committer = invocationContext.ServiceProvider.GetService<IStoreCommitter>();
if (committer is not null)
{
_logger.LogDebug("StoreCommitterHubFilter committing after hub method '{HubMethod}' on hub '{HubName}'.", invocationContext.HubMethodName, invocationContext.Hub.GetType().Name);
}
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("StoreCommitterHubFilter committing after hub method '{HubMethod}' on hub '{HubName}'.", invocationContext.HubMethodName, invocationContext.Hub.GetType().Name);
}

await committer.CommitAsync();
await committer.CommitAsync();
}

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<Sdk Name="Aspire.AppHost.Sdk" Version="13.2.2" />

<PropertyGroup>
<TargetFrameworks />
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<IsAspireHost>true</IsAspireHost>
<IsPackable>false</IsPackable>
Expand All @@ -19,6 +21,7 @@

<ItemGroup>
<ProjectReference Include="../CrestApps.Core.Mvc.Web/CrestApps.Core.Mvc.Web.csproj" />
<ProjectReference Include="../CrestApps.Core.Blazor.Web/CrestApps.Core.Blazor.Web.csproj" />
<ProjectReference Include="../CrestApps.Core.Mvc.Samples.A2AClient/CrestApps.Core.Mvc.Samples.A2AClient.csproj" />
<ProjectReference Include="../CrestApps.Core.Mvc.Samples.McpClient/CrestApps.Core.Mvc.Samples.McpClient.csproj" />
</ItemGroup>
Expand Down
15 changes: 15 additions & 0 deletions src/Startup/CrestApps.Core.Aspire.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ void WriteCrashEntry(string label, object data)
options.EnvironmentVariables.Add("CrestApps__MvcApp__A2A__Host__ExposeAgentsAsSkill", "true");
});

var blazorWeb = builder.AddProject<Projects.CrestApps_Core_Blazor_Web>("BlazorWeb")
.WithReference(redis)
.WithReference(ollama)
.WaitFor(redis)
.WithHttpsEndpoint(5200, name: "HttpsBlazorWeb")
.WithEnvironment((options) =>
{
options.EnvironmentVariables.Add("CrestApps__AI__Providers__Ollama__DefaultDeploymentName", ollamaModelName);
options.EnvironmentVariables.Add("CrestApps__AI__Providers__Ollama__Connections__Default__Endpoint", "http://localhost:11434");
options.EnvironmentVariables.Add("CrestApps__AI__Providers__Ollama__Connections__Default__ChatDeploymentName", ollamaModelName);
options.EnvironmentVariables.Add("CrestApps__BlazorApp__MCP__Server__AuthenticationType", "None");
options.EnvironmentVariables.Add("CrestApps__BlazorApp__A2A__Host__AuthenticationType", "None");
options.EnvironmentVariables.Add("CrestApps__BlazorApp__A2A__Host__ExposeAgentsAsSkill", "true");
});

builder.AddProject<Projects.CrestApps_Core_Mvc_Samples_McpClient>("MvcMcpClientSample")
.WithReference(mvcWeb)
.WaitFor(mvcWeb)
Expand Down
Loading
Loading