A transição brusca do Python/Django para o mundo tipado do C# e .NET 8. Um monorepo de 49 MB com Blazor, Capacitor, WebTorrent e SQL direto — e 89 commits depois, o que aprendi.
Vitor Neuenschwander
CS Student & Developer
Depois de meses confortável com Django e Python, o projeto "Educação Inteligente" me jogou no mundo do .NET 8 e C# 12. A transição foi brusca: de um mundo interpretado e dinâmico (dict["chave"]) para tipagem estática, compilação e Dependency Injection em tudo.
O projeto nasceu dentro da organização pai-educacao-inteligente no GitHub. Repositório privado, criado em agosto de 2025, com 89 commits e 5 contribuidores. O resultado? Um monorepo de ~49 MB com 4 componentes convivendo juntos — e funcionando.
educacao-inteligente/ (~49 MB, 89 commits)
├── EducacaoInteligente/ # Blazor WebAssembly — Frontend SPA
│ └── .NET 8.0 # Renderizado no browser via WASM
├── EducacaoInteligenteAPI/ # ASP.NET Core Minimal API — Backend
│ └── .NET 8.0 # PostgreSQL + JWT + RBAC
├── AndroidApplication/ # Capacitor 7.4.4 — Wrapper Mobile
│ └── com.Transcolar.EducacaoInteligente
├── VideoSharing/ # WebTorrent 2.8.4 — P2P de Vídeos
│ └── bittorrent-tracker
├── Executavel/ # Builds publicados
└── docs/ # Documentação
Composição por linguagem:
| Linguagem | Bytes | % |
|---|---|---|
| HTML | 276.338 | 60.7% |
| C# | 112.042 | 24.6% |
| CSS | 40.745 | 9.0% |
| JavaScript | 24.550 | 5.4% |
| Java | 1.314 | 0.3% |
.razor é basicamente HTML com C# embutido.
O ASP.NET Core Minimal API elimina controllers, startup pesadas e convenções implícitas. Cada endpoint é definido diretamente no Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Injeção de dependências modular
builder.Services.AddScoped<NpgsqlConnection>(sp =>
new NpgsqlConnection(
builder.Configuration.GetConnectionString("Default")
));
var app = builder.Build();
// Endpoints organizados por módulo
app.MapAuthEndpoints(); // /auth
app.MapAlunoEndpoints(); // /alunos
app.MapProfessorEndpoints(); // /professores
app.MapGestorEndpoints(); // /gestores (dashboard admin)
app.MapConteudoEndpoints(); // /conteudos
app.Run();
Cada MapXxxEndpoints() é um extension method que agrupa as rotas. Sem controllers de 500 linhas, sem herança pesada. Cada grupo é independente e testável.
Entity Framework Core é fantástico para prototipação rápida. Mas neste projeto, queria controle total sobre as queries e as transações. O Npgsql 8.0.7 permite injetar SQL diretamente no PostgreSQL:
app.MapGet("/api/alunos", [Authorize(Roles = "Professor,Gestor")]
async (NpgsqlConnection db) =>
{
await db.OpenAsync();
await using var cmd = new NpgsqlCommand(
"SELECT id, nome, turma FROM alunos ORDER BY nome", db
);
var reader = await cmd.ExecuteReaderAsync();
var alunos = new List<object>();
while (await reader.ReadAsync())
{
alunos.Add(new {
Id = reader.GetInt64(0),
Nome = reader.GetString(1),
Turma = reader.GetString(2)
});
}
return Results.Ok(alunos);
});
Sem overhead de ORM, sem geração de SQL implícita, sem surpresas de lazy loading, sem migrations automáticas. Cada query é intencional e otimizada.
Quando precisas saber exatamente que SQL está a ser executado e quando, Npgsql direto é imbatível.
A gestão de perfis (Aluno, Professor, Gestor) usa JWT com Role-Based Access Control. Cada endpoint declara explicitamente quem pode acessar:
// Configuração completa do JWT
builder.Services.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme
).AddJwtBearer(options =>
{
options.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:Key"]!
))
};
});
// Aplicação por endpoint:
// [Authorize(Roles = "Aluno")] → Só estudantes
// [Authorize(Roles = "Professor")] → Só professores
// [Authorize(Roles = "Professor,Gestor")] → Ambos
Pacotes de autenticação:
Microsoft.AspNetCore.Authentication.JwtBearer 8.0.8System.IdentityModel.Tokens.Jwt 8.0.2Microsoft.IdentityModel.Tokens 8.0.2Data Transfer Objects agem como filtros rigorosos na fronteira da API. Definidos como records do C# (imutáveis por design), só o que está no DTO entra:
// Envio de resposta pelo aluno
public record RespostaEnvio(
long IdAluno,
long IdQuestao,
string RespostaDada
);
// Associação de vídeo a conteúdo
public record AssociaVideo(
long IdConteudo,
long IdVideo
);
// Criação de conteúdo pelo professor
public record CriarConteudo(
long IdDisciplina,
string Nome,
string? Descricao // nullable!
);
// Criação de questão
public record CriarQuestao(
long IdConteudo,
string Enunciado,
string Alternativas
);
// Credenciais de login
public record Credenciais(
string Login,
string Senha
);
Se mandares um campo extra no JSON? Ignorado. Tipo errado? Erro de compilação — não em runtime como Python, mas no build. Segurança by design.
Juntando tudo — JWT, DTO, Npgsql — num endpoint real:
app.MapPost("/api/respostas", [Authorize(Roles = "Aluno")]
async (RespostaEnvio dto, NpgsqlConnection db) =>
{
await using var cmd = new NpgsqlCommand(
@"INSERT INTO respostas
(id_aluno, id_questao, resposta, data_envio)
VALUES (@aluno, @questao, @resp, NOW())", db);
cmd.Parameters.AddWithValue("aluno", dto.IdAluno);
cmd.Parameters.AddWithValue("questao", dto.IdQuestao);
cmd.Parameters.AddWithValue("resp", dto.RespostaDada);
await db.OpenAsync();
await cmd.ExecuteNonQueryAsync();
return Results.Created();
});
Decomposição:
[Authorize(Roles = "Aluno")] → Só alunos autenticados com JWT válidoRespostaEnvio dto → Deserialização automática + validação de tiposNpgsqlConnection db → Injetado via DI, sem new nem using manualO módulo VideoSharing usa webtorrent 2.8.4 e bittorrent-tracker 11.2.1 para distribuição peer-to-peer de vídeos:
import WebTorrent from 'webtorrent'
const client = new WebTorrent()
// Cada aluno que assiste um vídeo se torna um "seed"
client.seed(videoFile, { announce: trackerUrl }, (torrent) => {
console.log('Seeding:', torrent.magnetURI)
})
// Outros alunos baixam diretamente dos colegas
client.add(magnetURI, (torrent) => {
torrent.files[0].renderTo('video#player')
})
O resultado: quanto mais alunos assistem, mais rápido o vídeo carrega para os próximos — inversão completa do modelo servidor-centrado.
O frontend é inteiramente Blazor WebAssembly — C# compilado para WASM e executado no browser. Sem JavaScript framework, sem npm, sem webpack. Componentes .razor com lógica em C#:
@page "/dashboard"
@attribute [Authorize(Roles = "Aluno")]
<h1>Dashboard do Aluno</h1>
@if (disciplinas is null)
{
<p>Carregando...</p>
}
else
{
@foreach (var disc in disciplinas)
{
<DisciplinaCard Disciplina="@disc" />
}
}
@code {
private List<Disciplina>? disciplinas;
protected override async Task OnInitializedAsync()
{
disciplinas = await Http.GetFromJsonAsync<List<Disciplina>>(
"/api/disciplinas"
);
}
}
E com o Capacitor 7.4.4, o mesmo frontend Blazor é empacotado como app nativo Android (com.Transcolar.EducacaoInteligente).
O projeto teve 5 contribuidores:
| Contribuidor | Commits |
|---|---|
| ArthurMacielSs | 49 |
| neuxxkk (eu) | 19 |
| lucasvra | 11 |
| obrandemburg | 8 |
| Vitorelena | 2 |
A transição Python → C# foi dolorosa mas reveladora. Três conclusões:
records eliminam classes inteiras de bugs