📋 Índice
- Introdução e Objetivos
- Arquitetura do Sistema
- Modelos de Dados
- Base de Dados e ORM
- Autenticação e Autorização
- Aplicação Web — Razor Pages
- API REST
- Comunicação em Tempo Real — SignalR
- Interface e Estética
- Publicação e Infraestrutura
- Tecnologias e Dependências
- Padrões de Desenvolvimento
- Dificuldades e Soluções
- Melhorias e Evolução da Plataforma
- Conclusão
- Referências
1. Introdução e Objetivos
O projeto KapaDW foi desenvolvido no âmbito da unidade curricular de Desenvolvimento Web da Licenciatura em Engenharia Informática do Instituto Politécnico de Tomar. O objetivo central foi conceber e implementar uma plataforma completa de gestão de serviços web que demonstre a aplicação prática dos conteúdos lecionados ao longo do semestre.
A plataforma simula o modelo de negócio de uma agência digital: clientes registam-se na plataforma, consultam o catálogo de serviços disponíveis (desenvolvimento de websites, lojas online, SEO, entre outros), submetem pedidos com requisitos específicos e acompanham o progresso dos seus projetos em tempo real. Do lado da administração, o gestor tem visibilidade total sobre todos os pedidos, pode gerir o ciclo de vida de cada um, comunicar com os clientes através de um sistema de chat integrado, e gerir os próprios serviços e categorias oferecidos.
O projeto vai além de uma aplicação CRUD simples: inclui um sistema de notificações em tempo real baseado em WebSockets (SignalR), um workflow de aprovação de alterações para pedidos já submetidos, uma API REST documentada com Swagger, e um pipeline de publicação automatizado com Docker e Render.com, garantindo disponibilidade contínua.
- Implementação de autenticação e autorização baseada em roles com ASP.NET Identity
- Arquitetura de dois projetos: aplicação web (Razor Pages) + API REST (MVC Controllers)
- Comunicação em tempo real com SignalR (WebSockets)
- ORM Entity Framework Core com SQLite e migrations automáticas
- Publicação em cloud com Docker multi-stage e Render.com
- Interface responsiva com Bootstrap 5 e Tailwind CSS
Este relatório descreve em detalhe todos os componentes do sistema: arquitetura, modelos de dados, funcionalidades, interface, segurança e infraestrutura de publicação.
2. Arquitetura do Sistema
2.1 Visão Geral
O sistema é composto por dois projetos ASP.NET Core 8 que partilham a mesma base de dados SQLite e o mesmo conjunto de modelos de domínio. Esta separação segue o princípio da responsabilidade única: a aplicação web trata da interface com o utilizador, enquanto a API fornece acesso programático aos dados para integrações externas.
Porta 7001 (local)
Porta 10000 (cloud)
Bootstrap 5 SignalR JS jQuery
Entity Framework Core
Migrations automáticas
8 tabelas Seed Data
REST + Swagger
Porta 7002 (local)
Porta 10000 (cloud)
OpenAPI JSON
Figura 1 — Arquitetura geral do sistema com os dois projetos e a base de dados partilhada
O projeto API referencia diretamente o projeto WebServicos como dependência (via <ProjectReference> no ficheiro .csproj), o que lhe permite reutilizar os modelos (Models.cs), o contexto de base de dados (ApplicationDbContext), as migrations e as entidades Identity, sem duplicação de código.
2.2 Estrutura de Ficheiros
| Pasta / Ficheiro | Descrição |
|---|---|
WebServicos/WebServicos/ | Projeto principal — aplicação web Razor Pages |
WebServicos/WebServicos.API/ | Projeto API REST com Swagger |
Models/Models.cs | Todas as entidades do domínio |
Data/ApplicationDbContext.cs | Contexto EF Core, relações, seed de dados |
Data/DbSeeder.cs | Criação de roles e contas padrão |
Hubs/NotificacoesHub.cs | Hub SignalR para notificações em tempo real |
Pages/ | 26 ficheiros Razor Pages (.cshtml + .cshtml.cs) |
Pages/Shared/_Layout.cshtml | Template global com navbar, footer e SignalR JS |
Controllers/ (API) | PedidosController + ServicosController |
wwwroot/css/app.css | CSS gerado pelo Tailwind CSS (minificado) |
Dockerfile | Imagem Docker para o projeto web |
Dockerfile.api | Imagem Docker para a API |
render.yaml | Blueprint de deployment na Render.com |
2.3 Fluxo de uma Operação Típica
O ciclo de vida de um pedido ilustra como os vários componentes interagem entre si:
Figura 2 — Fluxo típico de um pedido desde o registo até à entrega
- O cliente autentica-se via formulário Identity (
/Identity/Account/Login) - Consulta o catálogo de serviços (
/Servicos), filtrável por pesquisa de texto - Submete um pedido (
/Pedidos/Create) selecionando serviços e preenchendo requisitos - A aplicação aciona o hub SignalR para notificar o administrador em tempo real
- O administrador analisa o pedido (
/Pedidos/Details), atualiza estados e comunica via chat - O cliente e o admin trocam mensagens no chat integrado ao pedido
- O admin marca como Concluído e introduz o URL de entrega do projeto
3. Modelos de Dados
Todos os modelos estão definidos no ficheiro WebServicos/WebServicos/Models/Models.cs, dentro do namespace WebServicos.Models. O ficheiro contém 7 classes e 2 enumerações, cobrindo todas as entidades do domínio.
3.1 ApplicationUser
Estende a classe IdentityUser do ASP.NET Core Identity, adicionando campos específicos da plataforma:
| Propriedade | Tipo | Validação | Descrição |
|---|---|---|---|
NomeCompleto | string | Required, MaxLength(150) | Nome completo do utilizador |
DataRegisto | DateTime | Default: UTC.Now | Data de registo na plataforma |
Pedidos | ICollection<Pedido> | Navigation | Pedidos submetidos pelo utilizador |
A herança de IdentityUser acrescenta automaticamente campos como Id (GUID), Email, PasswordHash, SecurityStamp, LockoutEnd, entre outros, geridos pelo framework.
3.2 Servico e CategoriaServico
A entidade Servico representa cada tipo de trabalho oferecido pela agência. Tem uma relação opcional com CategoriaServico (chave estrangeira nullable), o que permite que serviços existam sem categoria atribuída:
| Propriedade | Tipo | Validação | Descrição |
|---|---|---|---|
Id | int | PK auto-increment | Identificador único |
Nome | string | Required, Max(100) | Nome do serviço |
Descricao | string | Required, Max(1000) | Descrição detalhada |
PrecoBase | decimal(10,2) | Range(0, 99999.99) | Preço base em EUR |
Ativo | bool | Default: true | Visível para clientes |
DataCriacao | DateTime | Default: UTC.Now | Timestamp de criação |
Icone | string | Max(50) | Classe Bootstrap Icons (ex: "bi-globe") |
CategoriaServicoId | int? | FK nullable | Categoria a que pertence |
Foram inseridos 6 serviços pré-definidos via seed data, com preços reais de mercado:
| Nome | Preço Base | Ícone | Categoria |
|---|---|---|---|
| Website Institucional | € 799,00 | bi-globe | Websites |
| Loja Online | € 1.499,00 | bi-shop | E-commerce |
| Landing Page | € 349,00 | bi-layout-text-window | Websites |
| Otimização SEO | € 299,00 | bi-search | SEO & Marketing |
| Manutenção Mensal | € 89,00 | bi-tools | Manutenção |
| Recuperação de Software & Hardware | € 3.000,00 | bi-pc-display | Hardware & Software |
3.3 Pedido e PedidoServico
O Pedido é a entidade central do sistema, representando um projeto contratado por um cliente. A relação com os serviços é muitos-para-muitos, implementada através da tabela de junção PedidoServico, que acrescenta campos próprios (quantidade, preço acordado, notas por serviço):
| Propriedade (Pedido) | Tipo | Descrição |
|---|---|---|
TituloProjeto | string (Max 200) | Nome do projeto |
Descricao | string (Max 2000) | Requisitos detalhados |
Estado | EstadoPedido | Estado atual no workflow |
OrcamentoTotal | decimal? | Soma dos preços dos serviços no momento da criação |
PrazoEstimado | DateTime? | Data desejada de conclusão (opcional) |
EnderecoHttp | string? (URL) | URL do projeto entregue (preenchido pelo admin) |
ClienteId | string FK | Referência ao ApplicationUser |
| Propriedade (PedidoServico) | Tipo | Descrição |
|---|---|---|
PedidoId + ServicoId | int + int | Chave primária composta |
Quantidade | int (1-100) | Unidades do serviço contratadas |
PrecoAcordado | decimal? | Preço negociado (pode diferir do PrecoBase) |
Notas | string? (Max 500) | Requisitos específicos para este serviço |
3.4 Mensagem
A entidade Mensagem suporta o sistema de chat integrado a cada pedido. Cada mensagem pertence a um único pedido e tem um remetente identificado:
| Propriedade | Tipo | Descrição |
|---|---|---|
PedidoId | int FK | Pedido a que pertence esta mensagem |
RemetenteId | string FK | Utilizador que enviou a mensagem |
Conteudo | string (Max 2000) | Texto da mensagem |
DataHora | DateTime | Timestamp de envio (UTC) |
Lida | bool | Marcado como lido ao abrir o chat |
3.5 PedidoAlteracao
Esta entidade suporta o fluxo de alterações propostas pelo cliente em pedidos já submetidos. Em vez de editar diretamente os dados, o cliente propõe alterações que o administrador aprova ou rejeita:
| Propriedade | Tipo | Descrição |
|---|---|---|
TituloProjetoProposto | string? | Novo título proposto (opcional) |
DescricaoProposta | string? | Nova descrição proposta |
ObservacoesProposta | string? | Novas observações propostas |
Estado | EstadoAlteracao | Pendente / Aprovado / Rejeitado |
MotivoRejeicao | string? | Explicação da rejeição pelo admin |
DataDecisao | DateTime? | Data da decisão do admin |
DecididoPorId | string? FK | Admin que tomou a decisão |
3.6 Enumerações
O domínio usa duas enumerações para modelar o estado de pedidos e alterações, respetivamente:
EstadoPedido
| Valor | Int | Significado |
|---|---|---|
Pendente | 0 | Recebido, por analisar |
EmAnalise | 1 | Admin a analisar |
Aprovado | 2 | Aprovado, aguarda início |
EmDesenvolvimento | 3 | Em desenvolvimento |
EmRevisao | 4 | Aguarda revisão do cliente |
Concluido | 5 | Entregue e aceite |
Cancelado | 6 | Cancelado |
EstadoAlteracao
| Valor | Int | Significado |
|---|---|---|
Pendente | 0 | Aguarda decisão do admin |
Aprovado | 1 | Aprovado e aplicado |
Rejeitado | 2 | Rejeitado com motivo |
4. Base de Dados e ORM
4.1 ApplicationDbContext
O ApplicationDbContext herda de IdentityDbContext<ApplicationUser>, integrando automaticamente as tabelas do ASP.NET Identity (AspNetUsers, AspNetRoles, AspNetUserRoles, etc.) com as tabelas de domínio da aplicação. A classe está definida em Data/ApplicationDbContext.cs.
Os DbSet expostos são:
| Propriedade DbSet | Tabela gerada |
|---|---|
DbSet<Servico> Servicos | Servicos |
DbSet<CategoriaServico> CategoriasServico | CategoriasServico |
DbSet<Pedido> Pedidos | Pedidos |
DbSet<PedidoServico> PedidoServicos | PedidoServicos |
DbSet<Mensagem> Mensagens | Mensagens |
DbSet<PedidoAlteracao> PedidoAlteracoes | PedidoAlteracoes |
4.2 Relações e Constraints
O método OnModelCreating configura as relações entre entidades, incluindo chaves compostas e comportamentos de cascata:
// Chave primária composta da tabela de junção modelBuilder.Entity<PedidoServico>() .HasKey(ps => new { ps.PedidoId, ps.ServicoId }); // Pedido → User: Restrict (não apaga utilizador com pedidos) modelBuilder.Entity<Pedido>() .HasOne(p => p.Cliente) .WithMany(u => u.Pedidos) .HasForeignKey(p => p.ClienteId) .OnDelete(DeleteBehavior.Restrict); // Servico → Categoria: SetNull (categoria pode ser eliminada) modelBuilder.Entity<Servico>() .HasOne(s => s.CategoriaServico) .WithMany(c => c.Servicos) .HasForeignKey(s => s.CategoriaServicoId) .OnDelete(DeleteBehavior.SetNull);
A escolha de DeleteBehavior.Restrict para a relação Pedido→Utilizador protege a integridade dos dados: um utilizador com pedidos associados não pode ser eliminado diretamente. Já o DeleteBehavior.SetNull na relação Serviço→Categoria permite eliminar categorias sem perder serviços, colocando simplesmente o campo FK a NULL.
4.3 Seed de Dados
O DbSeeder.cs é responsável pela criação automática de roles e contas padrão na primeira execução. É chamado no arranque da aplicação (Program.cs) e é idempotente: pode ser executado múltiplas vezes sem criar duplicados.
public static async Task SeedAsync(IServiceProvider serviceProvider) { // Criar roles se não existirem await roleManager.CreateAsync(new IdentityRole(RoleAdmin)); await roleManager.CreateAsync(new IdentityRole(RoleCliente)); // Criar conta admin padrão var admin = new ApplicationUser { Email = "[email protected]", NomeCompleto = "Administrador do Sistema", EmailConfirmed = true }; await userManager.CreateAsync(admin, "Admin@123"); await userManager.AddToRoleAsync(admin, RoleAdmin); }
Os dados de seed de categorias e serviços são inseridos diretamente no OnModelCreating via modelBuilder.Entity<T>().HasData(...), que garante que são aplicados como parte das migrations do EF Core.
5. Autenticação e Autorização
5.1 ASP.NET Core Identity
A autenticação é gerida inteiramente pelo ASP.NET Core Identity, que fornece out-of-the-box: registo, login, logout, recuperação de password, bloqueio de conta e gestão de roles. A integração é feita através do scaffolding do Identity UI, que gera automaticamente as páginas Razor em /Identity/Account/.
A aplicação está configurada com dois roles: Administrador e Cliente. Cada utilizador pertence a exatamente um role, que determina as páginas e ações a que tem acesso.
5.2 Políticas e Convenções de Autorização
A autorização é configurada em Program.cs com uma combinação de convenções por pasta e atributos por classe:
// Convenções por pasta/página options.Conventions.AuthorizeFolder("/Pedidos"); options.Conventions.AuthorizeFolder("/Clientes", "Administrador"); options.Conventions.AuthorizePage("/Servicos/Create", "Administrador"); options.Conventions.AuthorizePage("/Servicos/Edit", "Administrador"); options.Conventions.AuthorizePage("/Servicos/Delete", "Administrador"); // Políticas nomeadas options.AddPolicy("Administrador", p => p.RequireRole("Administrador")); options.AddPolicy("Cliente", p => p.RequireRole("Cliente", "Administrador"));
Além das convenções globais, cada PageModel de áreas sensíveis usa o atributo [Authorize(Roles = "Administrador")] como segunda linha de defesa. Algumas páginas fazem verificações adicionais em runtime — por exemplo, em Pedidos/Edit, um cliente só pode editar pedidos no estado Pendente e o acesso a pedidos de outros clientes é bloqueado com Forbid().
5.3 Segurança de Passwords e Sessão
| Parâmetro | Valor |
|---|---|
| Comprimento mínimo | 8 caracteres |
| Dígito obrigatório | Sim (0–9) |
| Maiúscula obrigatória | Sim (A–Z) |
| Caráter especial obrigatório | Sim (!@#$%^&*) |
| Tentativas falhadas máximas | 5 |
| Duração do bloqueio | 15 minutos |
| Duração da sessão (cookie) | 8 horas |
| Confirmação de email | Desativada (demo) |
| Redirect não autenticado | /Identity/Account/Login |
| Redirect acesso negado | /Erro/AcessoNegado |
6. Aplicação Web — Razor Pages
A aplicação web segue o padrão Razor Pages, onde cada página tem um ficheiro .cshtml (view) e um ficheiro .cshtml.cs (page model com a lógica). Os handlers OnGet, OnPost e variantes nomeadas (OnPostAprovarAsync, OnPostAlterarEstadoAsync) implementam as ações de cada página. No total existem 26 ficheiros .cshtml em 7 áreas funcionais.
6.1 Gestão de Pedidos
A área de pedidos é o núcleo funcional da plataforma, com 7 páginas distintas:
| Página | Acesso | Funcionalidade Principal |
|---|---|---|
Pedidos/Index | Autenticado | Lista pedidos; admin vê todos, cliente vê só os seus. Carregamento com Include (eager loading) para evitar N+1 queries. |
Pedidos/Create | Cliente | Formulário de criação com seleção múltipla de serviços. Calcula orçamento automaticamente. Envolve criação em transação. Dispara notificação SignalR. |
Pedidos/Details | Autenticado | Visualização detalhada. Admin pode alterar estado e a aplicação notifica o cliente via SignalR direcionado. |
Pedidos/Edit | Autenticado | Cliente propõe alterações (via PedidoAlteracao); admin edita diretamente (prazo, estado, URL entrega). |
Pedidos/Delete | Autenticado | Cliente só cancela se Pendente; admin pode cancelar qualquer estado. |
Pedidos/Chat | Autenticado | Chat por pedido: envia mensagens, marca como lidas ao abrir, mostra histórico ordenado cronologicamente. |
Pedidos/RevisarAlteracoes | Admin | Lista e detalha propostas de alteração pendentes. Admin aprova (aplica ao pedido) ou rejeita (com motivo obrigatório). |
Lógica de Criação com Transação
O handler OnPostAsync de Pedidos/Create usa uma transação explícita para garantir atomicidade:
await using var transaction = await _db.Database.BeginTransactionAsync(); try { _db.Pedidos.Add(Pedido); await _db.SaveChangesAsync(); // Adicionar entradas na tabela de junção PedidoServico foreach (var id in ServicosIds) { _db.PedidoServicos.Add(new PedidoServico { PedidoId = Pedido.Id, ServicoId = id }); } await _db.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }
6.2 Catálogo de Serviços
A área de serviços tem comportamento diferenciado por role: clientes veem apenas serviços ativos; administradores veem todos (incluindo inativos), com indicação visual do estado. As páginas de criação, edição e eliminação são exclusivas de administradores.
A pesquisa no catálogo funciona via query string ?Pesquisa=... e filtra por Nome e Descricao com Contains() (LIKE no SQL). O resultado é ordenado por nome.
A eliminação de um serviço verifica primeiro se existem pedidos associados em PedidoServicos; se sim, bloqueia a operação com uma mensagem de erro clara, evitando violações de integridade referencial.
6.3 Gestão de Categorias
As categorias servem para organizar o catálogo de serviços. O CRUD completo (Index, Create, Edit, Delete) está disponível apenas para administradores. Na eliminação de uma categoria, os serviços associados mantêm-se na base de dados mas ficam sem categoria (CategoriaServicoId = NULL), graças ao comportamento SetNull configurado no contexto.
6.4 Gestão de Clientes
A página Clientes/Index apresenta ao administrador uma tabela com todos os utilizadores registados, incluindo o número total de pedidos, a data de registo e indicação de se é administrador. A verificação de role é feita via UserManager.IsInRoleAsync() para cada utilizador.
AspNetUserRoles.
6.5 Sistema de Alterações Propostas
Um dos aspetos mais interessantes do sistema é o workflow de alterações. Um cliente com um pedido em curso pode propor alterações ao título, descrição ou observações sem as aplicar diretamente. A proposta fica no estado Pendente até o administrador a analisar em Pedidos/RevisarAlteracoes.
Pendente
Figura 3 — Fluxo de aprovação de alterações propostas
6.6 Chat por Pedido
Cada pedido tem um chat privado entre o cliente e o administrador. As mensagens são persistidas na tabela Mensagens com controlo de estado de leitura (Lida). Ao abrir o chat, todas as mensagens não lidas são marcadas automaticamente como lidas via update em batch. O histórico é apresentado em ordem cronológica, com diferenciação visual entre mensagens enviadas e recebidas.
7. API REST
O projeto WebServicos.API expõe uma API RESTful documentada com Swagger, partilhando a base de dados com a aplicação web. É útil para integrações externas, dashboards de monitorização, ou acesso programático aos dados sem necessidade de navegar pela interface web.
7.1 PedidosController
Rota base: /api/pedidos
O endpoint GET /api/pedidos aceita dois query parameters opcionais: clienteId (filtra por utilizador) e estado (filtra pelo valor numérico do enum EstadoPedido). A resposta é uma projeção anónima que inclui dados do cliente e a lista de serviços associados, ordenada por data decrescente.
7.2 ServicosController
Rota base: /api/servicos
O endpoint DELETE inclui proteção de integridade: antes de eliminar, verifica se existem entradas em PedidoServicos e retorna 409 Conflict se sim, com mensagem explicativa. O endpoint de estatísticas é particularmente útil para dashboards externos, retornando dados agregados numa única chamada.
7.3 DTOs e Validação
A API utiliza Data Transfer Objects (DTOs) para separar os modelos de domínio da representação HTTP. O ServicoDto é usado para leitura e o CreateServicoDto para escrita (POST e PUT), com validação via Data Annotations:
public class CreateServicoDto { [Required] [StringLength(100)] public string Nome { get; set; } = ""; [Required] public string Descricao { get; set; } = ""; [Range(0, 99999.99)] public decimal PrecoBase { get; set; } public bool Ativo { get; set; } = true; public string Icone { get; set; } = "bi-globe"; }
7.4 Swagger / OpenAPI
A documentação da API é gerada automaticamente pelo Swashbuckle.AspNetCore (v6.5.0). Em ambiente de produção, o Swagger UI permanece ativo para facilitar a demonstração académica. A interface está acessível em /index.html e permite testar todos os endpoints diretamente no browser, sem necessidade de ferramentas externas como Postman.
A configuração CORS está definida como AllowAll para permitir chamadas de qualquer origem, adequado para contexto académico.
8. Comunicação em Tempo Real — SignalR
O SignalR é uma biblioteca ASP.NET Core que simplifica a adição de funcionalidades em tempo real usando WebSockets (com fallback para long-polling quando WebSockets não estão disponíveis). Na plataforma KapaDW, é usado para dois cenários:
- Notificação global: Quando um cliente submete um novo pedido, todos os administradores conectados recebem uma notificação imediata com o título do projeto e o nome do cliente.
- Notificação direcionada: Quando um administrador altera o estado de um pedido, o cliente proprietário desse pedido recebe uma notificação personalizada com o novo estado.
Hub: NotificacoesHub
O hub está registado no endpoint /hub/notificacoes e requer autenticação ([Authorize]):
/// Notifica todos os clientes conectados de um novo pedido public async Task NotificarNovoPedido(string tituloProjeto, string nomeCliente) { await Clients.All.SendAsync("NovoPedido", tituloProjeto, nomeCliente, DateTime.Now.ToString("HH:mm")); } /// Notifica apenas o cliente específico sobre atualização do seu pedido public async Task NotificarAtualizacaoPedido(string clienteId, string tituloProjeto, string novoEstado) { await Clients.User(clienteId).SendAsync("PedidoAtualizado", tituloProjeto, novoEstado, DateTime.Now.ToString("HH:mm")); }
No frontend (_Layout.cshtml), o cliente SignalR JavaScript está configurado com reconexão automática e um sistema de toasts que apresenta as notificações no canto superior direito com animação, desaparecendo automaticamente após 5 segundos:
connection.on("NovoPedido", (titulo, cliente, hora) => { mostrarNotificacao(`Novo pedido: "${titulo}" de ${cliente} às ${hora}`); incrementarBadge(); }); connection.on("PedidoAtualizado", (titulo, estado, hora) => { mostrarNotificacao(`O pedido "${titulo}" foi atualizado: ${estado} (${hora})`); });
A integração no fluxo de criação de pedido (Pedidos/Create) é feita server-side através da injeção do hub no page model:
private readonly IHubContext<NotificacoesHub> _hub; // Após guardar o pedido com sucesso: await _hub.Clients.All.SendAsync("NovoPedido", Pedido.TituloProjeto, nomeCliente);
9. Interface e Estética
9.1 Layout Global
Toda a aplicação utiliza um template único definido em Pages/Shared/_Layout.cshtml. O layout define a estrutura consistente de todas as páginas: navbar no topo, área de conteúdo central e footer. A fonte utilizada é Inter (Google Fonts), uma das fontes sans-serif mais usadas em produtos digitais modernos pela sua legibilidade em ecrãs.
A navbar adapta o seu conteúdo ao utilizador autenticado e ao seu role:
| Item de Menu | Visível Para | Rota |
|---|---|---|
| Início | Todos | /Index |
| Serviços | Todos | /Servicos/Index |
| Pedidos | Autenticados | /Pedidos/Index |
| Administração (dropdown) | Administradores | — (contém sub-itens) |
| ↳ Clientes | Admin | /Clientes/Index |
| ↳ Categorias | Admin | /Categorias/Index |
| ↳ Novo Serviço | Admin | /Servicos/Create |
| Sobre | Todos | /Sobre |
| Notificações (sino) | Autenticados | — (SignalR toast) |
9.2 Stack de Estilização
A interface usa uma abordagem híbrida com dois frameworks CSS complementares:
- Bootstrap 5.3.2 — Framework principal para grid, componentes (cards, modais, dropdowns, badges, alerts) e utilitários base. Incluído via CDN.
- Tailwind CSS 3.4 — Usado para estilos personalizados mais específicos, como a paleta de cores dark da página inicial, gradientes e efeitos de glassmorphism. O preflight está desativado para não conflituar com os resets do Bootstrap.
- Bootstrap Icons 1.11.3 — Biblioteca de ícones SVG usada em toda a interface para enriquecer visualmente menus, botões, listas e cartões.
- AOS (Animate On Scroll) — Biblioteca de animações de entrada ao scroll. Configurada com
duration: 650eonce: truepara animações suaves sem repetição.
O Tailwind é compilado em tempo de build via um target MSBuild personalizado no .csproj:
<Target Name="BuildTailwind" BeforeTargets="Build" Condition="Exists('package.json') And '$(SkipNpmBuild)' != 'true'"> <Exec Command="npm run build" WorkingDirectory="$(ProjectDir)" /> </Target>
A configuração do Tailwind em tailwind.config.js define uma paleta de cores customizada para o tema dark da homepage:
| Variável | Valor | Uso |
|---|---|---|
ws-bg | #070b14 | Fundo principal da homepage |
ws-surface | #111827 | Superfície de cards |
ws-blue | #3b82f6 | Cor de destaque principal |
ws-purple | #8b5cf6 | Cor de destaque secundária |
ws-cyan | #06b6d4 | Cor de destaque terciária |
ws-text | #f1f5f9 | Texto principal (dark mode) |
ws-muted | #64748b | Texto secundário |
9.3 Componentes Visuais Notáveis
Ao longo da interface, destacam-se vários padrões de design consistentes:
- Cards com sombra suave:
border-0 shadow-smsão usados em todo o conteúdo principal, criando profundidade sem bordas duras. - Cabeçalhos de card coloridos: Cada área tem cor própria (azul para academic info, verde para autores, amarelo para credenciais, info para stack).
- Feedback visual com TempData: Mensagens de sucesso (verde) e erro (vermelho) são persistidas entre redirects usando
TempDatae renderizadas no layout como alertas Bootstrap dismissíveis. - Badges de estado: Cada estado de pedido tem uma cor associada, tornando a lista de pedidos imediatamente legível.
- Ícones Bootstrap integrados: Praticamente todos os botões, itens de menu e cabeçalhos têm um ícone Bootstrap associado para melhorar a escaneabilidade visual.
- Responsive Design: O grid Bootstrap garante que a interface funciona corretamente em mobile, tablet e desktop.
9.4 Página de Início
A página inicial (Pages/Index.cshtml) tem um design distinto do restante da aplicação, com tema escuro e interativo. Inclui uma animação de partículas tech renderizada em canvas HTML5, onde partículas se movem e formam conexões ao aproximar o rato, criando uma estética futurista associada a tecnologia. A section hero usa a paleta Tailwind dark (ws-bg, ws-blue) com texto em gradiente e call-to-action buttons em destaque.
10. Publicação e Infraestrutura
10.1 Contentorização com Docker
Ambos os projetos são contentorizados com ficheiros Dockerfile separados, usando o padrão multi-stage build para produzir imagens leves de produção.
Dockerfile — Aplicação Web
# Stage 1: Build com SDK + Node.js (para compilar Tailwind CSS) FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs WORKDIR /src COPY WebServicos/WebServicos/package.json WebServicos/WebServicos/ RUN cd WebServicos/WebServicos && npm install COPY WebServicos/WebServicos/WebServicos.csproj WebServicos/WebServicos/ RUN dotnet restore WebServicos/WebServicos/WebServicos.csproj COPY . . WORKDIR /src/WebServicos/WebServicos RUN dotnet publish -c Release -o /app/publish --nologo # Stage 2: Runtime — imagem mínima sem SDK FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app COPY --from=build /app/publish . EXPOSE 10000 ENV ASPNETCORE_URLS=http://+:10000 ENTRYPOINT ["dotnet", "WebServicos.dll"]
A inclusão do Node.js na stage de build é necessária porque o target MSBuild BuildTailwind executa npm run build durante o dotnet publish, gerando o ficheiro wwwroot/css/app.css minificado.
Dockerfile.api — API REST
# Stage 1: Build (sem Node.js — SkipNpmBuild=true) FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY WebServicos/WebServicos/WebServicos.csproj WebServicos/WebServicos/ COPY WebServicos/WebServicos.API/WebServicos.API.csproj WebServicos/WebServicos.API/ RUN dotnet restore WebServicos/WebServicos.API/WebServicos.API.csproj COPY . . WORKDIR /src/WebServicos/WebServicos.API RUN dotnet publish -c Release -o /app/publish --nologo -p:SkipNpmBuild=true # Stage 2: Runtime FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app COPY --from=build /app/publish . EXPOSE 10000 ENV ASPNETCORE_URLS=http://+:10000 ENTRYPOINT ["dotnet", "WebServicos.API.dll"]
O flag -p:SkipNpmBuild=true é crucial: a API referencia o projeto WebServicos (para partilhar modelos), o que ativaria o target Tailwind. Como a API não tem interface web, não precisa de CSS, e o Node.js não está disponível neste Dockerfile.
10.2 Deployment na Render.com
A plataforma está publicada na Render.com, um serviço cloud que oferece um plano gratuito para containers Docker. A infraestrutura é definida declarativamente no ficheiro render.yaml (Render Blueprint), que descreve os dois serviços e as suas variáveis de ambiente:
services:
- type: web
name: webservicos
env: docker
dockerfilePath: ./Dockerfile
plan: free
envVars:
- key: ASPNETCORE_ENVIRONMENT
value: Production
- key: ConnectionStrings__DefaultConnection
value: Data Source=/app/webservicos.db
- type: web
name: webservicos-api
env: docker
dockerfilePath: ./Dockerfile.api
plan: free
envVars:
- key: ASPNETCORE_ENVIRONMENT
value: Production
- key: ConnectionStrings__DefaultConnection
value: Data Source=/app/webservicos.db
Os URLs de produção são:
- Aplicação Web:
https://webservicos.onrender.com - API REST:
https://webservicos-api.onrender.com
10.3 Pipeline de Entrega Contínua
O projeto usa dois repositórios GitHub com auto-deploy configurado:
| Remote Git | Repositório | Branch | Finalidade |
|---|---|---|---|
origin | kapa1233/projetoDW | master | Repositório principal do grupo |
diogo | diogogodinhowaze/projetodw | main | Ligado à Render para auto-deploy |
Sempre que é feito git push diogo master:main, a Render deteta a alteração e inicia automaticamente um novo build Docker e deploy dos dois serviços. O processo completo de build (inclui compilação .NET + npm build do Tailwind) demora tipicamente 3 a 5 minutos.
11. Tecnologias e Dependências
| Tecnologia | Versão | Papel no Projeto |
|---|---|---|
| ASP.NET Core | 8.0 | Framework base — Razor Pages e MVC Controllers |
| Entity Framework Core | 8.0 | ORM para acesso à base de dados e migrations |
| SQLite | — | Motor de base de dados (ficheiro único, sem servidor) |
| ASP.NET Core Identity | 8.0 | Autenticação, autorização, gestão de roles |
| ASP.NET Core SignalR | 8.0 | Comunicação em tempo real (WebSockets) |
| Swashbuckle.AspNetCore | 6.5.0 | Geração automática de documentação OpenAPI/Swagger |
| Bootstrap | 5.3.2 | Framework CSS — grid, componentes, utilitários |
| Bootstrap Icons | 1.11.3 | Biblioteca de ícones SVG inline |
| jQuery | 3.7.1 | Manipulação DOM, validação de formulários |
| Tailwind CSS | 3.4.x | Utilitários CSS customizados (paleta dark, gradientes) |
| AOS (Animate On Scroll) | 2.3.x | Animações de entrada ao scroll |
| Microsoft SignalR JS | 8.0.0 | Cliente JavaScript do SignalR |
| Node.js | 20.x | Build tool para compilar Tailwind CSS (apenas build) |
| Docker | — | Contentorização para deployment |
| Render.com | — | Plataforma cloud para hosting (plano gratuito) |
| GitHub | — | Controlo de versão + trigger de auto-deploy |
| Microsoft.Extensions.Hosting.WindowsServices | 8.0 | Suporte para execução como Windows Service (desenvolvimento local) |
12. Padrões de Desenvolvimento
12.1 Padrões Arquiteturais
- Repository via EF Core: O
ApplicationDbContextfunciona como unit of work e repositório implícito, sem camada adicional de repositório (adequado para a dimensão do projeto). - Page Model Pattern: Cada Razor Page tem um PageModel dedicado com handlers separados por ação (
OnGet,OnPost,OnPostAprovarAsync), mantendo a lógica de negócio separada da view. - DTO Pattern na API: Os controllers usam DTOs distintos para leitura (
ServicoDto) e escrita (CreateServicoDto), evitando expor modelos de domínio diretamente. - Eager Loading: As queries usam
.Include()e.ThenInclude()para carregar relações necessárias, evitando o problema N+1 queries.
12.2 Padrões de Dados
- Timestamps UTC: Todos os campos de data usam
DateTime.UtcNowpara consistência independente do fuso horário do servidor. - Soft vs Hard Delete: A aplicação usa hard delete em todos os casos, com proteção via constraints (
DeleteBehavior.Restrictonde necessário). - Seed idempotente: O
DbSeederverifica existência antes de criar roles e utilizadores, sendo seguro para executar múltiplas vezes. - Chave composta: A tabela de junção
PedidoServicousa uma chave primária composta (PedidoId + ServicoId), sem chave surrogate, o que é a prática correta para M:N puras com dados adicionais.
12.3 Padrões de UI
- Post-Redirect-Get (PRG): Todos os formulários de criação e edição redirecionam após POST bem sucedido, evitando re-submissão acidental ao recarregar a página.
- TempData para feedback: Mensagens de sucesso e erro são passadas entre redirects via
TempData, apresentadas como alerts Bootstrap na página seguinte. - Model Binding: O atributo
[BindProperty]liga automaticamente os campos do formulário HTML às propriedades do PageModel, eliminando código de parsing manual. - Validação dupla: A validação ocorre no lado do servidor (ModelState + lógica manual) e no cliente (jQuery Validation via
_ValidationScriptsPartial).
13. Dificuldades e Soluções
Durante o desenvolvimento e deployment do projeto, surgiram vários problemas técnicos que exigiram pesquisa e resolução criativa. Documentam-se aqui os mais relevantes:
| Problema | Causa | Solução |
|---|---|---|
| npm: not found durante Docker build | O target MSBuild BuildTailwind executa npm run build, mas a imagem Docker base (.NET SDK) não inclui Node.js |
Adicionar instalação de Node.js 20.x via NodeSource no Dockerfile da app web |
| npm ci falha por falta de package-lock.json | O ficheiro package-lock.json está no .gitignore e não é enviado para o repositório |
Substituir npm ci por npm install, que não requer o lock file |
| API falha no build Docker por npm: not found | A API referencia o projeto WebServicos, ativando o target Tailwind mesmo não precisando de CSS | Adicionar condição SkipNpmBuild=true ao target e passar o flag no dotnet publish da API |
| SQLite Error 14: unable to open database | O caminho relativo ../WebServicos/webservicos.db resolvia para /WebServicos/webservicos.db no container Docker (sem permissão) |
Sobrescrever a connection string nas env vars do render.yaml para Data Source=/app/webservicos.db |
| Render Blueprint "Resources already up to date" | Um serviço Node.js antigo existia no dashboard e bloqueava a criação dos serviços Docker | Eliminar o serviço obsoleto e sincronizar novamente o Blueprint |
| Render ligado ao repositório errado | O repositório principal do grupo (kapa1233/projetoDW) estava configurado na Render, mas o utilizador da Render era diogogodinhowaze |
Adicionar segundo remote Git (diogo) e fazer force push inicial para o repositório correto |
| SQLite perde dados a cada deploy/reinício na Render | O plano gratuito da Render não tem disco persistente — o filesystem do container é destruído a cada deploy ou quando o serviço adormece | Migrar para PostgreSQL gerido pela Render (plano free): adicionar secção databases: ao render.yaml com fromDatabase.connectionString nos dois serviços |
Npgsql rejeita o formato postgresql:// do DATABASE_URL |
A Render injeta a connection string no formato URL (postgresql://user:pass@host:port/db) mas o NpgsqlConnectionStringBuilder só aceita o formato ADO.NET (Host=...;Database=...;) |
Criar função ConvertDatabaseUrl() em ambos os Program.cs que converte o URL para formato ADO.NET antes de passar ao UseNpgsql() |
| Erro "timestamp with time zone literal cannot be generated for Unspecified DateTime" na migration | O Npgsql 8 usa timestamp with time zone por defeito, que exige DateTimeKind.Utc em todos os valores — incluindo os dados de seed (DataCriacao dos serviços) guardados na migration com Kind = Unspecified |
Ativar AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true) em ambos os Program.cs e na ApplicationDbContextFactory; regenerar as migrations com o switch ativo, que passa as colunas para timestamp without time zone sem restrição de UTC |
14. Melhorias e Evolução da Plataforma
Ao longo do desenvolvimento, a plataforma passou por melhorias concretas que a aproximam de um produto real. Esta secção documenta as evoluções mais significativas face à versão inicial.
14.1 Persistência de Dados com PostgreSQL
A alteração mais impactante foi a migração de SQLite para PostgreSQL como base de dados de produção. Esta mudança resolve um problema fundamental que existia na versão inicial da plataforma publicada na Render.com.
O problema com SQLite na cloud
Na versão original, a base de dados era um ficheiro SQLite (webservicos.db) armazenado no sistema de ficheiros do container Docker. O plano gratuito da Render não fornece disco persistente, o que significa que:
- Cada novo deploy apagava toda a base de dados — utilizadores registados, pedidos criados e mensagens de chat desapareciam
- O serviço adormece após 15 minutos de inatividade no plano gratuito; ao acordar, o container reiniciava e o ficheiro SQLite era destruído
- Apenas as contas de demonstração (admin e cliente demo) sobreviviam, porque são recriadas pelo seed automático a cada arranque
- Era impossível demonstrar continuidade — qualquer utilizador que se registasse antes de uma apresentação poderia já não existir no dia seguinte
A solução: PostgreSQL gerido pela Render
A solução foi integrar uma base de dados PostgreSQL gerida pela própria Render, independente dos containers das aplicações. O PostgreSQL é um serviço separado com disco persistente próprio — os dados vivem fora dos containers e sobrevivem a qualquer número de deploys, reinícios ou hibernações.
| Antes (SQLite) | Depois (PostgreSQL) | |
|---|---|---|
| Onde vivem os dados | Ficheiro no container Docker | Servidor PostgreSQL dedicado na Render |
| Utilizadores registados | Apagados a cada deploy/reinício | Persistem para sempre |
| Pedidos e mensagens | Perdidos com o container | Guardados permanentemente |
| Após hibernação (15 min) | Base de dados reiniciada do zero | Dados intactos — só o container acorda |
| Após novo deploy | Tudo apagado | Apenas o schema é atualizado (migrations) |
| Partilha entre serviços | Impossível (ficheiro local) | App web e API acedem à mesma BD |
Implementação técnica
A integração foi feita de forma a não quebrar o desenvolvimento local: a aplicação deteta automaticamente se está em produção (presença da variável DATABASE_URL injetada pela Render) e escolhe o provider adequado:
var databaseUrl = Environment.GetEnvironmentVariable("DATABASE_URL"); if (databaseUrl != null) { // Produção: PostgreSQL gerido pela Render var npgsqlConn = ConvertDatabaseUrl(databaseUrl); options.UseNpgsql(npgsqlConn); } else { // Local: SQLite — sem necessidade de instalar PostgreSQL options.UseSqlite(sqliteConnStr); }
O ficheiro render.yaml foi atualizado para declarar a base de dados e ligar automaticamente os dois serviços (app web e API) à mesma instância PostgreSQL:
databases:
- name: webservicos-db
plan: free
services:
- name: webservicos
envVars:
- key: DATABASE_URL
fromDatabase:
name: webservicos-db
property: connectionString
A gestão do schema é feita pelo EF Core Migrations: no arranque em produção, db.Database.Migrate() aplica automaticamente quaisquer alterações de schema pendentes, garantindo que a base de dados está sempre sincronizada com o código sem intervenção manual.
14.2 Outras Melhorias de Produto
| Melhoria | Descrição | Impacto |
|---|---|---|
| Página "Sobre" redesenhada | Substituição da tabela estática por layout com hero, diagrama de fluxo, cards de autores com avatares, badges de tecnologias e secção GitHub | Mais apelativa para avaliação académica; auto-explicativa para visitantes |
| Relatório técnico descarregável | Botão "Descarregar Relatório" na página Sobre serve o HTML diretamente de wwwroot/ |
Documentação acessível a partir da própria plataforma |
| Comentários XML em todo o código | Adição de /// <summary> em todas as classes, métodos e propriedades; comentários inline na lógica não óbvia |
Código legível no IntelliSense; facilita a avaliação e manutenção |
| Animação de partículas na homepage | Canvas HTML5 interativo com partículas que formam conexões ao aproximar o rato | Estética tech moderna, diferenciadora visualmente |
| Deploy automático (CI/CD) | Qualquer git push para o repositório desencadeia automaticamente um novo build e deploy na Render |
Zero intervenção manual para publicar novas versões |
15. Conclusão
O projeto KapaDW concretizou com sucesso os objetivos definidos para a unidade curricular de Desenvolvimento Web. A plataforma implementada vai além dos requisitos mínimos de um CRUD, incorporando funcionalidades características de sistemas web profissionais: comunicação em tempo real, workflows de aprovação, documentação de API, e deployment em cloud com CI/CD.
Do ponto de vista técnico, o projeto demonstrou competências em múltiplas camadas:
- Backend: Desenho de modelos de domínio com relações complexas (M:N com dados adicionais), configuração de ORM com EF Core, gestão de migrations, autorização granular com ASP.NET Identity, e implementação de uma API REST respeitando convenções HTTP.
- Tempo Real: Integração de SignalR tanto server-side (injeção via
IHubContext) como client-side (JavaScript com reconexão automática), com notificações globais e direcionadas. - Frontend: Interface responsiva com Bootstrap, animações com AOS, paleta dark customizada com Tailwind CSS, e sistema de feedback visual consistente com TempData.
- DevOps: Contentorização com Docker multi-stage (incluindo compilação de assets frontend), configuração de Blueprint na Render.com e pipeline de auto-deploy a partir de commits GitHub.
As maiores aprendizagens técnicas foram na área de contentorização com Docker, onde a necessidade de incluir Node.js na imagem de build (para compilar Tailwind CSS num ambiente .NET) e de gerir caminhos de base de dados absolutos no container exigiram um diagnóstico cuidado dos logs de erro e compreensão do comportamento do sistema de ficheiros em containers.
https://webservicos.onrender.com, com deploy automático a cada push para o repositório GitHub, demonstrando um fluxo de desenvolvimento moderno e profissional.
16. Referências
-
[1] Microsoft. ASP.NET Core Razor Pages. Microsoft Learn.
learn.microsoft.com/aspnet/core/razor-pages -
[2] Microsoft. Entity Framework Core — Getting Started. Microsoft Learn.
learn.microsoft.com/ef/core -
[3] Microsoft. ASP.NET Core Identity. Microsoft Learn.
learn.microsoft.com/aspnet/core/security/authentication/identity -
[4] Microsoft. ASP.NET Core SignalR Introduction. Microsoft Learn.
learn.microsoft.com/aspnet/core/signalr/introduction -
[5] Bootstrap Team. Bootstrap 5 Documentation.
getbootstrap.com/docs/5.3 -
[6] Tailwind Labs. Tailwind CSS Documentation.
tailwindcss.com/docs -
[7] SQLite Consortium. SQLite Documentation.
sqlite.org/docs.html -
[8] Domaindrivendesign.org. Domain-Driven Design Reference.
domainlanguage.com/ddd/reference -
[9] Docker Inc. Best practices for writing Dockerfiles.
docs.docker.com/develop/develop-images/dockerfile_best-practices -
[10] Render. Render Documentation — Blueprint Spec.
render.com/docs/blueprint-spec