eQuantic.UI - Arquitetura🌐 Esta página em: English · Português [!NOTE] Esta página descreve o pipeline web. O framework também tem como alvo o nativo (macOS/iOS/Android) a partir das mesmas fontes de componente. Veja Componentes write-once para a arquitetura compartilhada e Photon para o motor de GPU. O eQuantic.UI é um framework de interface autocontido para .NET que compila C# em JavaScript otimizado, eliminando dependências de Node.js, npm, Vite ou qualquer ferramenta de frontend externa.
O diagrama acima é uma cerca mermaid `: o GitHub o desenha aqui, e o site de documentação desenha a MESMA cerca pelo componente Mermaid do próprio SDK. 1.
✅ 100% .NET - zero dependências externas (Node.js, npm, etc)
2.
✅ Autocontido - o ASP.NET Core serve e compila tudo
3.
✅ Familiar - roteamento por atributos (como os Controllers)
4.
✅ Moderno - experiência de SPA com SSR quando preciso
5.
✅ Performático - compilação inteligente (estático vs dinâmico)
<!-- eQuantic.UI.Sdk herda do Microsoft.NET.Sdk.Web -->
<Project Sdk="eQuantic.UI.Sdk/1.0.0">
<TargetFramework>net9.0</TargetFramework>
Por baixo do capô:
<!-- eQuantic.UI.Sdk/Sdk/Sdk.props -->
<!-- Herda o Web SDK completo -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<!-- Acrescenta a compilação da interface -->
<EnableEQuanticUICompilation>true</EnableEQuanticUICompilation>
<EQuanticOutputPath>wwwroot/_equantic/</EQuanticOutputPath>
1.2 Integração com o pipeline de build
Pipeline padrão do MSBuild (Microsoft.NET.Sdk.Web)
Target próprio: CompileEQuanticUI (BeforeTargets="Build")
1. Roslyn faz o parse de /Pages/**/*.cs
2. Detecta as classes StatefulComponent/StatelessComponent/ComponentState
3. Gera o intermediário TypeScript (arquivos .ts)
└─ Legível por humanos (depuração)
4. Invoca o Bun embarcado
├─ bun build *.ts --outdir wwwroot/_equantic
├─ Tree-shaking automático
Saída: bin/ + wwwroot/_equantic/
Por que um intermediário TypeScript?
C# (fonte) → TypeScript (intermediário) → JavaScript (saída)
Quem escreve Tipos seguros Runtime
escreve C# + fácil de depurar otimizado
Benefícios:
•
✅ Checagem de tipos em duas camadas (C# + TS)
•
✅ Source maps de C# → TS → JS (depuração completa)
•
✅ Aproveita o motor de otimização do Bun
•
✅ Futuro: poderia suportar autoria direta em TS também
Performance do Bun:
# Build tradicional em Node.js
# Build com o Bun embarcado
⏱️ 1.8s ✅ (8,5x mais rápido)
2. Estratégia de compilação: estático vs dinâmico
2.1 Problema: bundle único vs divisão de código
Desafio: não queremos um bundle.js gigante, mas também não queremos centenas de arquivinhos.
Solução: estratégia de compilação híbrida
A. Casca estática (compilada em tempo de build)
O que compila estaticamente:
•
A estrutura do componente (a árvore de componentes)
•
A estrutura de layout/interface
•
Os metadados de roteamento
Saída: {ComponentName}.static.js
// Counter.static.js (gerado no build)
export const CounterStatic = {
// Estrutura do template (não precisa de runtime)
props: { className: "counter" },
{ type: "Heading", props: { text: "Counter" } },
{ type: "TextInput", props: { id: "msg", placeholder: "..." } },
{ type: "Button", props: { id: "dec", text: "-" } },
{ type: "Text", props: { id: "count", text: "0" } },
{ type: "Button", props: { id: "inc", text: "+" } },
// Estilo (CSS-in-JS compilado)
.counter { padding: 20px; }
.count-display { font-size: 24px; font-weight: bold; }
B. Lógica dinâmica (compilada no build, executa no cliente)
O que compila como lógica dinâmica:
•
Ganchos de ciclo de vida
Saída: {ComponentName}.logic.js
// Counter.logic.js (gerado no build)
export class CounterLogic {
this._component = component;
this._component.update({ count: this._count });
this._component.update({ count: this._count });
_onMessageChange(value) {
// Sem update se não reflete na interface
C. Server Actions (executam no servidor)
O que NÃO compila para JS:
•
Consultas ao banco de dados
•
Lógica de negócio complexa
•
Chamadas a APIs internas
•
Autenticação/autorização
Solução: o padrão Server Actions
public class TodoList : StatefulComponent
// Server Action - roda no servidor
public async Task<List<Todo>> LoadTodos()
using var db = new AppDbContext();
return await db.Todos.ToListAsync();
public async Task<Todo> AddTodo(string title)
using var db = new AppDbContext();
var todo = new Todo { Title = title };
await db.SaveChangesAsync();
public class TodoListState : ComponentState<TodoList>
private List<Todo> _todos = [];
protected override void OnMount()
private async Task LoadInitialData()
_todos = await Component.LoadTodos();
private async Task HandleAdd(string title)
var newTodo = await Component.AddTodo(title);
SetState(() => _todos.Add(newTodo));
public override IComponent Build(RenderContext context)
Children = _todos.Select(t =>
(IComponent)new TodoItem { Todo = t }
Compilação:
export class TodoListLogic {
// Gera a chamada ao server action
this._todos = await this._serverActions.invoke("LoadTodos", []);
const newTodo = await this._serverActions.invoke("AddTodo", [title]);
this._todos.push(newTodo);
this._component.update({ todos: this._todos });
2.2 Estratégia de bundles
Objetivo: otimizar o carregamento sem explodir a contagem de requisições.
Nível 1: núcleo do runtime (carrega em todas as páginas)
/_equantic/runtime.js (~15kb gzipado)
- Ponte dos server actions
Nível 2: biblioteca de componentes (carregada sob demanda por rota)
/_equantic/components.js (~30kb gzipado)
- Button, TextBox, Container, etc
- Componentes usados por várias páginas
Nível 3: bundles de página (carregados sob demanda por rota)
/_equantic/pages/Counter.js
- Counter.static.js (estrutura)
- Counter.logic.js (comportamento)
- Componentes específicos do Counter
Nível 4: assets externos (CDNs/scripts compartilhados)
https://cdn.example.com/library.js
- Declarados via IRequireAssets
- Deduplicados pela AssetCollection
Nível 4: chunks compartilhados (divisão automática de código)
- auth.chunk.js (se várias páginas usam auth)
- api.chunk.js (lógica de API compartilhada)
Exemplo de carregamento:
<!-- Requisição: GET /counter -->
<script src="/_equantic/runtime.js"></script>
<script src="/_equantic/components.js"></script>
<script src="/_equantic/pages/Counter.js"></script>
<!-- Navegação SPA: /counter → /todos -->
<script src="/_equantic/pages/TodoList.js"></script>
3. Server Actions: comunicação cliente ↔ servidor
3.1 Problema: evitar endpoints manuais
Antipadrão (que queremos evitar):
public class TodoController : ControllerBase
public Task<Todo> AddTodo([FromBody] AddTodoRequest req) { }
async function addTodo(title) {
const response = await fetch('/api/todos', {
body: JSON.stringify({ title })
return await response.json();
O que queremos (tipos seguros, zero boilerplate):
public class TodoList : StatefulComponent
public async Task<Todo> AddTodo(string title)
// Lógica de backend aqui
private async Task HandleAdd()
var todo = await Widget.AddTodo("New item");
// ↑ Tipos seguros, serialização automática
3.2 Implementação: a ponte dos Server Actions
A. Tempo de compilação
O compilador detecta os métodos com [ServerAction]:
public class Counter : StatefulComponent
public async Task<int> IncrementOnServer(int current)
// Simula lógica no servidor
Gera:
export class CounterLogic {
async incrementOnServer(current) {
return await this._serverActions.invoke(
"Counter/IncrementOnServer", // ID da ação
B. Em execução: o endpoint dos Server Actions
Middleware automático expondo /api/_equantic/actions:
// eQuantic.UI.Server/ServerActionsMiddleware.cs
public class ServerActionsMiddleware
private readonly IServerActionRegistry _registry;
public async Task InvokeAsync(HttpContext context)
if (context.Request.Path == "/api/_equantic/actions")
var request = await JsonSerializer
.DeserializeAsync<ServerActionRequest>(context.Request.Body);
// request.ActionId = "Counter/IncrementOnServer"
// request.Arguments = [5]
var action = _registry.GetAction(request.ActionId);
// Invoca o método via reflexão (ou expressão compilada)
var result = await action.InvokeAsync(request.Arguments);
await context.Response.WriteAsJsonAsync(new {
C. A ponte no runtime do cliente
// runtime.js - a ponte dos Server Actions
class ServerActionsClient {
async invoke(actionId, args) {
const response = await fetch("/api/_equantic/actions", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ actionId, arguments: args }),
const data = await response.json();
throw new Error(data.error);
3.3 Push em tempo real: escopo
A versão atual não inclui um modelo de assinatura [ServerEvent]: os Server Actions são requisição/resposta. Os serviços do SignalR são registrados e usados internamente pelo framework; o push servidor→cliente em tempo real está no Roadmap. Os componentes declaram as próprias dependências externas (scripts, folhas de estilo) sem injeção manual no shell HTML. Dois padrões são suportados:
Padrão 1: IRequireAssets, em que um componente declara os assets dele:
public class CodeBlock : StatelessComponent, IRequireAssets
public void ConfigureAssets(AssetBuilder assets)
assets.AddStylesheet("https://cdn.example.com/prism.css", id: "prism-theme");
assets.AddScript("https://cdn.example.com/prism.js");
assets.AddInlineScript("function init(){ ... }");
Padrão 2: IComponentAssetProvider<T>, um provedor externo para componentes de terceiros:
public class ChartJsAssetProvider : IComponentAssetProvider<ChartCanvas>
public void ConfigureAssets(AssetBuilder assets)
assets.AddScript("https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js");
Os provedores são registrados automaticamente durante o AddUI() pela varredura de assemblies, ou explicitamente via WithAssetProvider<T>().
Fluxo do framework:
1.
O ServerRenderingService percorre a árvore de componentes, coletando os assets de IRequireAssets e de IComponentAssetProvider<T>.
2.
A AssetCollection deduplica as entradas por Key (CSS primeiro, depois JS).
3.
O UIExtensions injeta as tags resultantes no <head> da página.
4.
Páginas sem componentes que exigem assets não ganham tag extra nenhuma.
4. Diferenças em relação ao ASP.NET WebForms
4.1 Aprendizados do WebForms
O que o WebForms fazia bem:
•
✅ Modelo de eventos (onClick, onChange)
•
✅ Controles de servidor com estado
•
✅ Postback para lógica de servidor
O que o WebForms fazia mal:
•
❌ ViewState gigante (aumenta a carga)
•
❌ Postback de página inteira (não é SPA)
•
❌ HTML gerado no servidor (lento)
•
❌ JavaScript limitado/difícil
4.2 Como o eQuantic.UI melhora isso
Aspecto
WebForms
eQuantic.UI
Gestão de estado
ViewState (campo escondido)
Estado no cliente + Server Actions
Renderização
Geração de HTML no servidor
Renderização no cliente (DOM virtual)
Atualizações
Postback completo
Atualizações parciais (SPA)
Integração com JS
UpdatePanel/ScriptManager
Compilação nativa para JavaScript
Tratamento de eventos
Postback no servidor
No cliente + Server Actions seletivos
Performance
Todo clique = ida ao servidor
Lógica no cliente, servidor quando preciso
Tamanho do bundle
N/A (renderizado no servidor)
Mínimo (~15kb de runtime)
4.3 O melhor dos dois mundos
Experiência de desenvolvimento parecida com o WebForms:
// Familiar para quem vem do WebForms
public class Counter : StatefulComponent
private void OnButtonClick() // ← Como no WebForms!
// Mas roda no cliente, sem postback!
Performance de SPA moderna:
// Compilado para JS otimizado
// Roda no browser, sem postback
// Só chama o servidor quando realmente preciso
5. Roteamento e sistema de páginas
5.1 Roteamento por atributos
[Page("/count")] // Várias rotas
public class Counter : StatefulComponent { }
[Page("/user/{id:int}")] // Parâmetros de rota
public class UserProfile : StatefulComponent
public int Id { get; set; } // Binding automático
// Pages/Admin/Dashboard.cs
[Page("/admin/dashboard")]
[Authorize(Roles = "Admin")] // Autorização
public class AdminDashboard : StatefulComponent { }
5.2 Registro no Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddUI(options => {
options.ScanAssembly(typeof(Program).Assembly);
var app = builder.Build();
app.MapUI(); // Descoberta automática pelos atributos [Page] + fallback SPA
5.3 Navegação (no cliente)
A navegação no cliente é tratada pelo roteador do runtime: cliques em links e o componente Link navegam sem recarga, com parâmetros de rota tipados, guardas, prefetch e restauração de rolagem. Uma API Navigator programática e tipada não faz parte da versão atual.
6. Experiência de desenvolvimento
├── MyApp.csproj # eQuantic.UI.Sdk
├── Program.cs # Host do ASP.NET Core
├── Pages/ # Componentes de página
│ ├── Home.cs # [Page("/")]
│ ├── Counter.cs # [Page("/counter")]
│ └── Dashboard.cs # [Page("/admin/dashboard")]
├── Components/ # Componentes de interface reutilizáveis
├── Services/ # Serviços de backend (DI)
├── Models/ # Modelos compartilhados
└── wwwroot/ # Assets estáticos
├── _equantic/ # Gerado (saída do build)
6.2 Comandos de linha de comando
dotnet new install eQuantic.UI.Templates
dotnet new equantic-app -n MyApp
dotnet new equantic-page -n UserProfile -o Pages
dotnet new equantic-component -n DataGrid -o Components
# → Hot reload nas mudanças de .cs
# → Recompilação automática para JS
# → Atualização automática do browser
dotnet publish -c Release
1. A pessoa edita o Counter.cs
2. O dotnet watch detecta a mudança
3. A task do MSBuild recompila Counter.cs → Counter.js
4. O observador de arquivos avisa o browser (SSE em `/_equantic/hmr`)
5. O browser busca o Counter.js atualizado
6. Hot Module Replacement
7. A interface atualiza sem perder o estado
7.1 Bun embarcado para a compilação de TypeScript
Decisão: usar o Bun como ferramenta de build embarcada
Por que o Bun:
•
✅ Executável único - distribui junto com o SDK
•
✅ Ultrarrápido - 10 a 100x mais rápido que o Node.js
•
✅ TypeScript nativo - compila TS sem configuração
•
✅ Bundler embutido - não precisa de Webpack/Vite
•
✅ Autocontido - nenhum npm install é necessário
•
✅ Pegada pequena - ~90MB (contra ~200MB do Node.js)
Arquitetura:
│ └── eqc-compiler.ts # Wrapper do compilador TypeScript
└── eQuantic.UI.Build.targets
Pipeline de build com o Bun:
Task do MSBuild: CompileEQuanticUI
1. Parse do Roslyn no C# (Pages/**/*.cs + fontes da biblioteca)
2. Gera o intermediário TypeScript
3. O Bun embarcado empacota → wwwroot/_equantic/