-
Notifications
You must be signed in to change notification settings - Fork 1
Architecture pt BR
🌐 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.
graph LR
A[componentes C#] --> B{eqc}
B -->|casca estatica| C[HTML do SSR]
B -->|logica dinamica| D(TypeScript)
D --> E{Bun embarcado}
E --> F[wwwroot/_equantic]
C --> G((Browser))
F --> G
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.
- ✅ 100% .NET - zero dependências externas (Node.js, npm, etc)
- ✅ Autocontido - o ASP.NET Core serve e compila tudo
- ✅ Familiar - roteamento por atributos (como os Controllers)
- ✅ Moderno - experiência de SPA com SSR quando preciso
- ✅ 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">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>Por baixo do capô:
<!-- eQuantic.UI.Sdk/Sdk/Sdk.props -->
<Project>
<!-- Herda o Web SDK completo -->
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<!-- Acrescenta a compilação da interface -->
<PropertyGroup>
<EnableEQuanticUICompilation>true</EnableEQuanticUICompilation>
<EQuanticOutputPath>wwwroot/_equantic/</EQuanticOutputPath>
</PropertyGroup>
</Project>dotnet 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)
├─ Com tipos seguros
├─ Preserva a semântica
└─ Legível por humanos (depuração)
4. Invoca o Bun embarcado
├─ bun build *.ts --outdir wwwroot/_equantic
├─ Tree-shaking automático
├─ Minificação
├─ Source maps
└─ Divisão de código
5. Gera o manifest.json
↓
Continua o build padrão
↓
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
$ npm run build
⏱️ 15.3s
# Build com o Bun embarcado
$ dotnet build
⏱️ 1.8s ✅ (8,5x mais rápido)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
O que compila estaticamente:
- A estrutura do componente (a árvore de componentes)
- A estrutura de layout/interface
- Os estilos
- O estado inicial
- Os metadados de roteamento
Saída: {ComponentName}.static.js
// Counter.static.js (gerado no build)
export const CounterStatic = {
name: "Counter",
route: "/counter",
// Estrutura do template (não precisa de runtime)
template: {
type: "Container",
props: { className: "counter" },
children: [
{ type: "Heading", props: { text: "Counter" } },
{ type: "TextInput", props: { id: "msg", placeholder: "..." } },
{
type: "Row",
props: { gap: "8px" },
children: [
{ type: "Button", props: { id: "dec", text: "-" } },
{ type: "Text", props: { id: "count", text: "0" } },
{ type: "Button", props: { id: "inc", text: "+" } },
],
},
],
},
// Estilo (CSS-in-JS compilado)
styles: `
.counter { padding: 20px; }
.count-display { font-size: 24px; font-weight: bold; }
`,
};O que compila como lógica dinâmica:
- Handlers de evento
- Mutações de estado
- Propriedades computadas
- Ganchos de ciclo de vida
Saída: {ComponentName}.logic.js
// Counter.logic.js (gerado no build)
export class CounterLogic {
constructor(component) {
this._component = component;
this._count = 0;
this._message = "";
}
// Handlers compilados
_increment() {
this._count++;
this._component.update({ count: this._count });
}
_decrement() {
this._count--;
this._component.update({ count: this._count });
}
_onMessageChange(value) {
this._message = value;
// Sem update se não reflete na interface
}
}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
// Pages/TodoList.cs
[Page("/todos")]
public class TodoList : StatefulComponent
{
// Server Action - roda no servidor
[ServerAction]
public async Task<List<Todo>> LoadTodos()
{
// Roda no servidor
using var db = new AppDbContext();
return await db.Todos.ToListAsync();
}
[ServerAction]
public async Task<Todo> AddTodo(string title)
{
using var db = new AppDbContext();
var todo = new Todo { Title = title };
db.Todos.Add(todo);
await db.SaveChangesAsync();
return todo;
}
}
public class TodoListState : ComponentState<TodoList>
{
private List<Todo> _todos = [];
protected override void OnMount()
{
// Chama o server action
_ = LoadInitialData();
}
private async Task LoadInitialData()
{
_todos = await Component.LoadTodos();
SetState(() => { });
}
private async Task HandleAdd(string title)
{
var newTodo = await Component.AddTodo(title);
SetState(() => _todos.Add(newTodo));
}
public override IComponent Build(RenderContext context)
{
return new Column {
Children = _todos.Select(t =>
(IComponent)new TodoItem { Todo = t }
).ToList()
};
}
}Compilação:
// TodoList.logic.js
export class TodoListLogic {
async onMounted() {
// Gera a chamada ao server action
this._todos = await this._serverActions.invoke("LoadTodos", []);
}
async handleAdd(title) {
const newTodo = await this._serverActions.invoke("AddTodo", [title]);
this._todos.push(newTodo);
this._component.update({ todos: this._todos });
}
}Objetivo: otimizar o carregamento sem explodir a contagem de requisições.
/_equantic/runtime.js (~15kb gzipado)
- DOM virtual mínimo
- Sistema de eventos
- Gestão de estado
- Ponte dos server actions
/_equantic/components.js (~30kb gzipado)
- Button, TextBox, Container, etc
- Componentes usados por várias páginas
/_equantic/pages/Counter.js
- Counter.static.js (estrutura)
- Counter.logic.js (comportamento)
- Componentes específicos do Counter
https://cdn.example.com/library.js
- Declarados via IRequireAssets
- Deduplicados pela AssetCollection
- Injetados no <head>
/_equantic/chunks/
- 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 -->
<!-- Só carrega: -->
<script src="/_equantic/pages/TodoList.js"></script>Antipadrão (que queremos evitar):
// Backend
[ApiController]
public class TodoController : ControllerBase
{
[HttpPost("/api/todos")]
public Task<Todo> AddTodo([FromBody] AddTodoRequest req) { }
}
// Frontend (JS)
async function addTodo(title) {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title })
});
return await response.json();
}O que queremos (tipos seguros, zero boilerplate):
[Page("/todos")]
public class TodoList : StatefulComponent
{
[ServerAction]
public async Task<Todo> AddTodo(string title)
{
// Lógica de backend aqui
}
}
// No frontend:
private async Task HandleAdd()
{
var todo = await Widget.AddTodo("New item");
// ↑ Tipos seguros, serialização automática
}O compilador detecta os métodos com [ServerAction]:
// Counter.cs
public class Counter : StatefulComponent
{
[ServerAction]
public async Task<int> IncrementOnServer(int current)
{
// Simula lógica no servidor
await Task.Delay(100);
return current + 1;
}
}Gera:
// Counter.logic.js
export class CounterLogic {
async incrementOnServer(current) {
return await this._serverActions.invoke(
"Counter/IncrementOnServer", // ID da ação
[current], // Argumentos
);
}
}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 {
success = true,
result = result
});
return;
}
await _next(context);
}
}// runtime.js - a ponte dos Server Actions
class ServerActionsClient {
async invoke(actionId, args) {
const response = await fetch("/api/_equantic/actions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ actionId, arguments: args }),
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error);
}
return data.result;
}
}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:
- O
ServerRenderingServicepercorre a árvore de componentes, coletando os assets deIRequireAssetse deIComponentAssetProvider<T>. - A
AssetCollectiondeduplica as entradas porKey(CSS primeiro, depois JS). - O
UIExtensionsinjeta as tags resultantes no<head>da página. - Páginas sem componentes que exigem assets não ganham tag extra nenhuma.
Veja Gestão de assets para a documentação completa.
O que o WebForms fazia bem:
- ✅ Modelo de eventos (onClick, onChange)
- ✅ ViewState automático
- ✅ 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
| 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) |
Experiência de desenvolvimento parecida com o WebForms:
// Familiar para quem vem do WebForms
public class Counter : StatefulComponent
{
private int _count = 0;
private void OnButtonClick() // ← Como no WebForms!
{
_count++;
// 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// Pages/Counter.cs
[Page("/counter")]
[Page("/count")] // Várias rotas
public class Counter : StatefulComponent { }
// Pages/UserProfile.cs
[Page("/user/{id:int}")] // Parâmetros de rota
public class UserProfile : StatefulComponent
{
[Parameter]
public int Id { get; set; } // Binding automático
}
// Pages/Admin/Dashboard.cs
[Page("/admin/dashboard")]
[Authorize(Roles = "Admin")] // Autorização
public class AdminDashboard : StatefulComponent { }// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddUI(options => {
options.ScanAssembly(typeof(Program).Assembly);
});
var app = builder.Build();
app.UseStaticFiles();
app.UseServerActions();
app.MapUI(); // Descoberta automática pelos atributos [Page] + fallback SPA
app.Run();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.
MyApp/
├── MyApp.csproj # eQuantic.UI.Sdk
├── Program.cs # Host do ASP.NET Core
│
├── Pages/ # Componentes de página
│ ├── Home.cs # [Page("/")]
│ ├── Counter.cs # [Page("/counter")]
│ └── Admin/
│ └── Dashboard.cs # [Page("/admin/dashboard")]
│
├── Components/ # Componentes de interface reutilizáveis
│ ├── Button.cs
│ ├── Card.cs
│ └── DataGrid.cs
│
├── Services/ # Serviços de backend (DI)
│ ├── UserService.cs
│ └── ApiClient.cs
│
├── Models/ # Modelos compartilhados
│ └── User.cs
│
└── wwwroot/ # Assets estáticos
├── _equantic/ # Gerado (saída do build)
│ ├── runtime.js
│ ├── components.js
│ └── pages/
│ ├── Counter.js
│ ├── Home.js
└── css/
└── site.css
# Instala o template
dotnet new install eQuantic.UI.Templates
# Cria um app novo
dotnet new equantic-app -n MyApp
cd MyApp
# Cria uma página nova
dotnet new equantic-page -n UserProfile -o Pages
# Cria um componente
dotnet new equantic-component -n DataGrid -o Components
# Desenvolvimento
dotnet watch run
# → Hot reload nas mudanças de .cs
# → Recompilação automática para JS
# → Atualização automática do browser
# Build
dotnet build
# → Compila o C# para JS
# → Otimiza os bundles
# → Gera o manifesto
# Publicação
dotnet publish -c Release
# → JS minificado
# → Tree-shaking
# → Pronto para produção1. 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
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:
eQuantic.UI.Sdk/
├── tools/
│ ├── bun.exe (Windows)
│ ├── bun (Linux)
│ ├── bun (macOS)
│ └── eqc-compiler.ts # Wrapper do compilador TypeScript
└── build/
└── eQuantic.UI.Build.targets
Pipeline de build com o Bun:
dotnet build
↓
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/
Veja Fluxo de build para o pipeline completo, passo a passo.
🌐 English · Português
🏁 Comece aqui
📱 Write-once
- Componentes write-once
- Superfície declarativa
- Motor Photon
- Design System
- Capacidades
- Armazenamento
- Formulários
- Editor de código
- Markdown
- Mermaid
- Renderização de Email
🏗️ Arquitetura
⚙️ Compilação
- Compilador
- Avaliação em tempo de compilação
- Recursos C# suportados
- Resolução de tipos externos
- Fluxo de build
- Diagnósticos
⚡ Runtime
🔌 Servidor
🎨 Ecossistema
🚀 Desenvolvimento