Skip to content

Architecture pt BR

Edgar Mesquita edited this page Aug 14, 2026 · 2 revisions

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.

Visão geral

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
Loading

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.

Princípios centrais

  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)

1. Arquitetura do SDK

1.1 Hierarquia do SDK

<!-- 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>

1.2 Integração com o pipeline de build

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)

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 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; }
  `,
};

B. Lógica dinâmica (compilada no build, executa no cliente)

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
  }
}

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

// 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 });
  }
}

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)
  - DOM virtual mínimo
  - Sistema de eventos
  - Gestão de estado
  - 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
  - Injetados no <head>

Nível 4: chunks compartilhados (divisão automática de código)

/_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>

3. Server Actions: comunicação cliente ↔ servidor

3.1 Problema: evitar endpoints manuais

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
}

3.2 Implementação: a ponte dos Server Actions

A. Tempo de compilação

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
    );
  }
}

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 {
                success = true,
                result = result
            });

            return;
        }

        await _next(context);
    }
}

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", {
      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;
  }
}

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.

3.4 Gestão de assets

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.

Veja Gestão de assets para a documentação completa.


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)
  • ✅ 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

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 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

5. Roteamento e sistema de páginas

5.1 Roteamento por atributos

// 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 { }

5.2 Registro no Program.cs

// 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();

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

6.1 Estrutura do projeto

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

6.2 Comandos de linha de comando

# 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ção

6.3 Fluxo do hot reload

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. Decisões técnicas

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:

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.

Clone this wiki locally