Skip to content

Arquitetura

rodrigosdk edited this page Nov 12, 2026 · 3 revisions

Arquitetura: como o código está organizado e por quê

Para quem é este artigo: para quem vai navegar pelo código pela primeira vez, e para quem quer entender Clean Architecture com um exemplo real, e não com caixinhas abstratas.

Pré-requisitos: saber o que é uma classe e uma interface em C# (ou em qualquer linguagem orientada a objetos) ajuda. Ter lido Entendendo o negócio ajuda mais ainda.

O que você vai aprender: o que é arquitetura de software, a regra de dependência, o papel de cada projeto da solução, o caminho completo de uma requisição e como as peças são ligadas.


Afinal, o que é "arquitetura"?

Em um projeto pequeno, dá para colocar tudo em um arquivo: recebe a requisição, valida, faz o SELECT, aplica a regra, faz o UPDATE, responde. Funciona, até o dia em que:

  • você precisa testar a regra "sinistro negado não pode ser pago" e descobre que, para isso, precisa de um banco de dados rodando e de uma requisição HTTP;
  • a mesma regra aparece copiada em três lugares, e alguém corrige só dois;
  • trocar de banco, ou só atualizar a biblioteca de acesso a dados, mexe em metade do sistema.

Arquitetura é a decisão de como dividir o código em partes, o que cada parte pode saber das outras, e por quê. O objetivo não é elegância: é que mudanças fiquem baratas e localizadas. Uma mudança na regra de negócio deve tocar um lugar; uma troca de banco deve tocar outro.


A ideia central: a regra de dependência

O PolicyDesk segue a Clean Architecture, proposta por Robert C. Martin ("Uncle Bob"). Ela é parente próxima da Arquitetura Hexagonal (Alistair Cockburn) e da Onion Architecture (Jeffrey Palermo). As três dizem, com palavras diferentes, a mesma coisa:

O código só pode depender de coisas mais internas. O núcleo, onde ficam as regras de negócio, não sabe que existe banco de dados, HTTP ou framework.

Pense nas camadas de uma cebola. No centro, as regras do negócio de seguros, que só mudariam se o negócio mudasse. Em volta, a orquestração das operações. Na casca, os detalhes técnicos: o banco, a web, as bibliotecas. A casca conhece o centro; o centro nunca conhece a casca.

flowchart TB
    subgraph Externo["Casca: frameworks e detalhes técnicos"]
        API["PolicyDesk.Api<br/>(ASP.NET Core, HTTP)"]
        INF["PolicyDesk.Infrastructure<br/>(EF Core, PostgreSQL)"]
    end
    subgraph Aplicacao["Meio: orquestração"]
        APP["PolicyDesk.Application<br/>(casos de uso)"]
    end
    subgraph Nucleo["Centro: regras de negócio"]
        DOM["PolicyDesk.Domain<br/>(entidades, regras, contratos)"]
    end

    API --> APP
    API --> INF
    APP --> DOM
    INF --> DOM
Loading

As setas são dependências de código: "A aponta para B" significa que A usa tipos de B. Repare que todas apontam para dentro.

Mas como o domínio salva dados sem conhecer o banco?

Esta é a pergunta que todo mundo faz, e a resposta é o truque mais importante da arquitetura: inversão de dependência.

O domínio declara uma interface, um contrato que diz "eu preciso de alguém que saiba buscar um sinistro pelo id":

// Domain: só o contrato
public interface IClaimsReadOnlyRepository
{
    Task<Claim?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
}

A infraestrutura, na casca, implementa esse contrato usando o banco:

// Infrastructure: a implementação com EF Core
internal sealed class ClaimsRepository(PolicyDeskDbContext dbContext) : IClaimsReadOnlyRepository /* ... */
{
    public Task<Claim?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        return dbContext.Claims.AsNoTracking().FirstOrDefaultAsync(claim => claim.Id == id, cancellationToken);
    }
}

A dependência de código vai da infraestrutura para o domínio (a classe implementa uma interface do domínio), mas, quando o programa roda, quem faz o trabalho é a infraestrutura. Quem liga uma coisa à outra é a injeção de dependência, explicada mais abaixo.

O que se ganha com isso

Benefício Como aparece no PolicyDesk
Testar sem banco A classe Claim é testada com new Claim(...) e nada mais. Os casos de uso são testados com repositórios falsos (mocks). Centenas de testes rodam em segundos
Trocar detalhes técnicos Trocar o EF Core por Dapper exigiria reescrever só a Infrastructure. O domínio e os casos de uso não mudariam uma linha
Regras em um só lugar "Só sinistros aprovados podem ser pagos" existe em um único método: Claim.RegisterPayment()
O código conta o que o sistema faz A pasta Application/Claims mostra Register, Analysis, Approve, Deny, Payment, Listing. Uncle Bob chama isso de Screaming Architecture: a estrutura "grita" o negócio, não o framework

Os projetos da solução, um por um

src/
├── Backend/
│   ├── PolicyDesk.Domain            ← o centro: regras de negócio
│   ├── PolicyDesk.Application       ← os casos de uso
│   ├── PolicyDesk.Infrastructure    ← banco de dados
│   └── PolicyDesk.Api               ← HTTP e a montagem de tudo
└── Shared/
    ├── PolicyDesk.Communication     ← o formato do JSON de entrada e saída
    └── PolicyDesk.Exception         ← exceções e mensagens de erro

Em .NET, cada projeto vira uma assembly (uma DLL). Um projeto só pode usar tipos de outro se declarar uma referência (ProjectReference) no seu .csproj. Isso transforma a regra de dependência em algo que o compilador verifica: se alguém tentar usar o EF Core dentro do Domain, o código não compila, porque o Domain não referencia o EF Core.

O diagrama abaixo mostra exatamente as referências que existem nos .csproj:

flowchart LR
    API[Api] --> APP[Application]
    API --> INF[Infrastructure]
    API --> EXC[Exception]
    APP --> DOM[Domain]
    APP --> COM[Communication]
    APP --> EXC
    INF --> DOM
    DOM --> EXC
Loading

PolicyDesk.Domain: o coração

Aqui mora o conhecimento sobre seguros, escrito em C# puro.

  • Entidades (Entities/): Policyholder, Policy e Claim. Elas não são só "sacos de dados": cada uma protege as próprias regras. Claim.Approve() se recusa a aprovar um sinistro que não está em análise.
  • Enums (Enums/): PolicyStatus e ClaimStatus, os estados possíveis.
  • Validadores (Validations/): CpfValidator, EmailValidator, PhoneValidator, FieldLimits e MoneyLimits.
  • Relógio de negócio (SeedWorks/BusinessClock): responde "que dia é hoje" no fuso de Brasília.
  • Contratos (Repositories/): as interfaces dos repositórios e do IUnitOfWork, que a infraestrutura implementa.

Depende apenas do projeto Exception (para lançar erros com mensagens traduzidas) e da biblioteca libphonenumber-csharp (para validar telefones). Aprofunde em Modelo de Domínio.

PolicyDesk.Application: os casos de uso

Um caso de uso é uma operação que o usuário do sistema quer fazer: "aprovar um sinistro", "cadastrar um segurado". Cada um vira uma classe com um único método público:

Application/
├── Claims/         Register · Listing · Analysis · Approve · Deny · Payment
├── Policies/       Register · Listing · Cancel
├── Policyholders/  Register · Listing · Update · Deactivate
└── Common/         Pagination · StatusMapping

Cada pasta tem uma interface pública (IApproveClaimUseCase) e uma implementação internal (ApproveClaimUseCase), com o método ExecuteAsync (ou ListAsync, nas listagens).

O caso de uso não contém regras de negócio; ele orquestra. Valida a entrada, busca as entidades, pede para a entidade fazer a mudança, confirma a gravação e registra o log. É como um gerente que distribui o trabalho, mas não executa as tarefas técnicas.

A pasta Common/ guarda o que vários casos de uso compartilham: a validação e a montagem da paginação, e a tradução dos status do domínio (InAnalysis) para os da API (EmAnalise).

PolicyDesk.Infrastructure: o banco de dados

Implementa os contratos do domínio usando o Entity Framework Core (EF Core), a biblioteca da Microsoft que traduz classes C# em tabelas e consultas SQL.

  • PolicyDeskDbContext e os mapeamentos (Mappings/*Map.cs), que dizem, por exemplo, que a propriedade CoverageAmount vira a coluna coverage_amount.
  • Os repositórios (Repositories/), que implementam as interfaces do domínio.
  • O UnitOfWork, que grava tudo de uma vez e traduz erros do banco em erros de negócio.
  • As migrations, que versionam a estrutura do banco.

Quase tudo aqui é internal: o resto da solução só enxerga as interfaces e um único método público, AddInfrastructure. Aprofunde em Persistência e Banco de Dados.

PolicyDesk.Api: a porta de entrada

A camada que fala HTTP, e o lugar onde todas as peças são montadas (a raiz de composição).

  • Controllers/: recebem a requisição, chamam o caso de uso e escolhem o status HTTP. São propositalmente "finos": nenhuma regra mora aqui.
  • Filters/ExceptionFilter e Errors/ProblemDetailsSetup: transformam exceções em respostas de erro padronizadas.
  • Observability/: logs, traces, métricas e o cabeçalho X-Trace-Id.
  • Converters/StringConverter: limpa os espaços de todo texto recebido.
  • Program.cs: monta o pipeline HTTP e registra as dependências.

PolicyDesk.Communication: o contrato público

Os objetos que definem o formato do JSON: Request*Json (o que entra) e Response*Json (o que sai), com nomes em português, e os enums da API (StatusApolice, StatusSinistro).

Esse projeto não depende de nenhum outro. Isso permite, por exemplo, que um cliente .NET use esses mesmos tipos para chamar a API, sem levar o domínio junto. E garante que renomear algo no domínio não mude a API sem querer.

PolicyDesk.Exception: erros e mensagens

A hierarquia de exceções (PolicyDeskException e suas filhas) e os arquivos de mensagens: ResourcesMessageException.resx (português, o padrão) e ResourcesMessageException.en.resx (inglês). Aprofunde em Tratamento de Erros.


A viagem de uma requisição, do começo ao fim

Teoria ajuda, mas nada substitui seguir uma requisição de verdade. Vamos acompanhar a aprovação de um sinistro: POST /api/sinistros/{id}/aprovacao.

sequenceDiagram
    autonumber
    participant C as Cliente
    participant MW as Middlewares
    participant CT as ClaimsController
    participant UC as ApproveClaimUseCase
    participant R as ClaimsRepository
    participant E as Claim (entidade)
    participant U as UnitOfWork
    participant DB as PostgreSQL

    C->>MW: POST /api/sinistros/{id}/aprovacao
    Note over MW: X-Trace-Id · log · tratamento de erros · idioma
    MW->>CT: Approve(id)
    CT->>UC: ExecuteAsync(id, ct)
    UC->>R: GetByIdForUpdateAsync(id)
    R->>DB: SELECT ... (com rastreamento)
    DB-->>R: linha + versão (xmin)
    R-->>UC: Claim
    UC->>E: Approve()
    Note over E: está em análise? então Status = Approved,<br/>ApprovedAt = agora
    UC->>U: CommitAsync(ct)
    U->>DB: UPDATE ... WHERE id = @id AND xmin = @versao
    DB-->>U: 1 linha afetada
    UC-->>CT: (sem retorno)
    CT-->>C: 204 No Content + X-Trace-Id
Loading

Passo a passo, em português:

  1. Os middlewares recebem a requisição antes de qualquer código nosso. Eles preparam o identificador da requisição, começam a medir o tempo, ficam de prontidão para capturar erros e leem o idioma pedido.
  2. O roteamento do ASP.NET olha a URL e escolhe o método Approve do ClaimsController. O trecho {id:guid} da rota garante que o id seja um GUID válido.
  3. O controller só chama o caso de uso. Não há if nenhum ali.
  4. O caso de uso pede ao repositório o sinistro, "para alteração".
  5. O repositório faz o SELECT. Junto com os dados, vem a versão da linha (xmin), usada mais tarde para detectar conflitos.
  6. A entidade recebe a ordem Approve(). É ela quem decide se pode: se o sinistro não estiver em análise, lança um erro de regra de negócio. Se puder, muda o próprio estado.
  7. O Unit of Work grava. O UPDATE só acontece se a versão da linha ainda for a mesma que foi lida.
  8. O controller responde 204 No Content: deu certo, nada a devolver.

E se algo der errado no caminho?

Situação Onde é percebida O que é lançado Resposta
O id não é um GUID Roteamento — 404
O sinistro não existe ApproveClaimUseCase NotFoundException 404
O sinistro não está em análise Claim.Approve() DomainRuleException 400
Outra requisição alterou o sinistro no meio UnitOfWork.CommitAsync (nenhuma linha afetada) ConflictException 409
O cliente desistiu e fechou a conexão CancellationToken OperationCanceledException 499
Um erro inesperado ExceptionFilter qualquer outra exceção 500 (detalhes só no log)

Cada camada lança uma exceção que descreve o problema ("não encontrado", "regra violada"), sem saber nada de HTTP. Um único lugar, na borda, traduz isso para status HTTP. Veja Tratamento de Erros.


O pipeline HTTP: a ordem importa

Um middleware é uma etapa pela qual toda requisição passa, na ida e na volta, como as estações de uma esteira. Cada um pode fazer algo antes de passar adiante e algo depois que a resposta volta. A ordem em que são registrados no Program.cs é a ordem da esteira:

app.UseCorrelationIdHeader();   // 1. garante o X-Trace-Id em toda resposta
app.UseHttpLogging();           // 2. registra método, rota, status e duração
app.UseErrorHandling();         // 3. rede de segurança para exceções
app.UseRequestLocalization();   // 4. define o idioma a partir do Accept-Language
// Swagger (em Development ou com Swagger:Enabled=true)
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");

Por que o log (2) vem antes do tratamento de erros (3)? Porque, na volta, ele fica por fora. Se um erro vira uma resposta 500 dentro do passo 3, o passo 2 vê o 500 final e registra corretamente. Na ordem inversa, o log registraria a resposta antes do tratamento e mostraria um status errado.

Outros detalhes do Program.cs que vale conhecer:

  • Corpo de no máximo 64 KB. Nenhuma requisição legítima chega perto disso. O limite protege contra quem tenta derrubar a API com payloads enormes.
  • URLs em minúsculas (LowercaseUrls = true).
  • Migrations automáticas só fora de produção. Em produção, o pipeline de deploy aplica as migrations em um passo próprio. Veja CI/CD e Deploy.
  • public partial class Program { }, no fim do arquivo, existe só para os testes de integração conseguirem referenciar a classe Program.

Injeção de dependência: quem cria quem

Repare que nenhum caso de uso faz new ClaimsRepository(...). Eles só pedem o que precisam no construtor:

internal class ApproveClaimUseCase(
    IClaimsUpdateOnlyRepository claimsUpdateOnlyRepository,
    IUnitOfWork unitOfWork,
    ILogger<ApproveClaimUseCase> logger) : IApproveClaimUseCase

Quem cria os objetos e entrega as dependências certas é o contêiner de injeção de dependência do ASP.NET. Para isso, cada camada registra suas peças:

// Application/DependencyInjectionExtension.cs
services.AddScoped<IApproveClaimUseCase, ApproveClaimUseCase>();

// Infrastructure/DependencyInjectionExtension.cs
services.AddScoped<IClaimsUpdateOnlyRepository, ClaimsRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddDbContext<PolicyDeskDbContext>(...);

Lê-se: "quando alguém pedir um IClaimsUpdateOnlyRepository, entregue um ClaimsRepository". O Program.cs só chama AddInfrastructure(...) e AddApplication().

Por que "Scoped"? E por que não Singleton ou Transient?

O contêiner precisa saber quanto tempo cada objeto vive. Existem três opções:

Tempo de vida Quando o contêiner cria uma instância nova Exemplo no PolicyDesk
Singleton Uma vez, e a mesma instância serve a aplicação inteira, até ela ser desligada BusinessClock, TimeProvider
Scoped Uma vez por requisição HTTP. Dentro da mesma requisição, todos recebem a mesma instância; a próxima requisição recebe outra Casos de uso, repositórios, UnitOfWork, DbContext
Transient Toda vez que alguém pede, mesmo dentro da mesma requisição (não usado)

Para entender a escolha, comece pela peça central: o DbContext. Ele não é só uma "conexão com o banco". Ele guarda o change tracker, a lista das entidades carregadas e do que mudou em cada uma. É essa lista que o SaveChanges consulta para montar os UPDATEs. Por isso, a pergunta certa é: quem precisa enxergar o mesmo DbContext?

Na aprovação de um sinistro, duas peças diferentes usam o DbContext:

sequenceDiagram
    participant UC as ApproveClaimUseCase
    participant R as ClaimsRepository
    participant U as UnitOfWork
    participant C as DbContext

    UC->>R: GetByIdForUpdateAsync(id)
    R->>C: carrega o sinistro (fica no change tracker)
    UC->>UC: claim.Approve()  (muda o objeto em memória)
    UC->>U: CommitAsync()
    U->>C: SaveChanges(): "o que mudou?"
    Note over C: só funciona se for o MESMO DbContext<br/>que carregou o sinistro
Loading

O repositório carrega; o UnitOfWork salva. Os dois precisam compartilhar a mesma instância. Agora, veja o que acontece em cada opção.

Se o DbContext fosse Transient

Cada pedido criaria um DbContext novo. O repositório receberia um, e o UnitOfWork, outro:

ClaimsRepository  → DbContext A  (carregou o sinistro; sabe que ele mudou)
UnitOfWork        → DbContext B  (vazio; nunca viu o sinistro)

UnitOfWork.CommitAsync() → DbContext B.SaveChanges() → "nada mudou" → nenhum UPDATE

A aprovação não seria gravada, e o pior: sem erro nenhum. A API responderia 204, como se tudo tivesse dado certo. Esse é o tipo de defeito mais perigoso, porque ninguém percebe até alguém notar o dado errado no banco. Além disso, cada DbContext extra abre e gerencia sua própria conexão, o que desperdiça recursos.

Se o DbContext (ou um repositório) fosse Singleton

Uma única instância seria compartilhada por todas as requisições, de todos os usuários, ao mesmo tempo. Os problemas aparecem em cascata:

  1. O DbContext não é thread-safe. Ele não foi feito para ser usado por duas threads ao mesmo tempo. Com duas requisições simultâneas, o EF Core lança InvalidOperationException: A second operation was started on this context instance before a previous operation completed, ou, pior, corrompe o estado interno.
  2. Uma requisição gravaria as mudanças da outra. O change tracker é compartilhado: se a requisição A alterou um sinistro e falhou antes de salvar, a requisição B, ao chamar SaveChanges, gravaria também a alteração pela metade de A.
  3. Os dados envelheceriam. O change tracker guarda as entidades carregadas para sempre. Com o tempo, a memória cresce sem parar, e as consultas rastreadas podem devolver uma versão antiga, já em memória, em vez da versão atual do banco.

E repare: basta um repositório ser Singleton para causar tudo isso, mesmo com o DbContext registrado como Scoped. O repositório Singleton é criado uma única vez, na primeira requisição, e recebe o DbContext daquela requisição. Ele guarda esse DbContext para sempre e o usa em todas as requisições seguintes. Esse defeito tem nome: captive dependency (dependência capturada). Um objeto de vida longa "aprisiona" um objeto que deveria viver pouco.

A regra geral: um serviço nunca deve depender de outro com tempo de vida mais curto que o dele. Singleton pode depender de Singleton. Scoped pode depender de Scoped ou de Singleton (é o caso dos casos de uso que recebem o BusinessClock). Singleton nunca deve depender de Scoped. Em Development, o ASP.NET confere essa regra ao subir (ValidateScopes) e lança um erro se encontrar uma dependência capturada.

Scoped: exatamente o tamanho certo

Com Scoped, dentro de uma requisição, o repositório e o UnitOfWork recebem o mesmo DbContext, e o SaveChanges enxerga tudo. Entre requisições, cada uma tem o seu, então ninguém vê nem grava as mudanças de ninguém. O "escopo" de uma requisição coincide com o de uma operação de negócio, que é exatamente a unidade de trabalho que queremos gravar de uma vez.

E os repositórios e casos de uso? Não poderiam ser Transient?

Poderiam, e funcionariam. Essa é uma sutileza que vale entender: o que precisa ser compartilhado é o DbContext, e não o repositório. Se um repositório fosse Transient, cada pedido criaria um repositório novo, mas o contêiner entregaria a todos eles o mesmo DbContext Scoped da requisição. Nada quebraria.

Mesmo assim, o PolicyDesk registra repositórios e casos de uso como Scoped, por três motivos:

  1. Não há ganho em Transient. Esses objetos não guardam estado próprio; criar várias instâncias deles na mesma requisição só gera trabalho extra para o coletor de lixo.
  2. Consistência. Tudo o que depende do DbContext vive o mesmo tempo que ele. Quem lê os registros não precisa se perguntar por que um é Transient e o outro, Scoped.
  3. Segurança contra mudanças futuras. Se um dia alguém acrescentar estado a um repositório (um cache por requisição, por exemplo), Scoped já é o comportamento correto. Com Transient, cada consumidor teria uma cópia diferente desse estado, um defeito difícil de enxergar.

Transient é a escolha natural para objetos leves, sem estado e que não dependem de nada com escopo, como um formatador ou uma calculadora. O PolicyDesk simplesmente não tem esse tipo de serviço registrado no contêiner: os validadores são classes estáticas.

Opção para o DbContext O que acontece
Transient Repositório e UnitOfWork recebem instâncias diferentes: as alterações somem sem erro
Singleton Uma instância para todos os usuários: erros de concorrência, gravações misturadas, memória crescendo
Scoped (usado) Compartilhado dentro da requisição, isolado entre requisições

💡 Para experimentar: troque, em Infrastructure/DependencyInjectionExtension.cs, o registro do IUnitOfWork para AddSingleton e rode os testes de integração. Depois registre o PolicyDeskDbContext com ServiceLifetime.Transient (AddDbContext<PolicyDeskDbContext>(..., ServiceLifetime.Transient)) e rode de novo. Compare os dois tipos de falha: um barulhento, outro silencioso.

Os controllers recebem os casos de uso direto no método, com [FromServices], e não no construtor. Assim, cada action resolve só o caso de uso que usa. Mais detalhes em Design Patterns › Injeção de Dependência.


Concessões conscientes

Nenhuma arquitetura real é igual à do livro. Estes são os desvios do PolicyDesk, e por que foram aceitos:

  1. A Api referencia a Infrastructure. É preciso para chamar AddInfrastructure e rodar as migrations no Program.cs. A regra continua valendo onde importa: nenhum controller usa tipos da infraestrutura.
  2. Os casos de uso recebem os objetos do JSON. RegisterClaimUseCase recebe um RequestClaimJson. É uma simplificação comum; em sistemas maiores, a camada de aplicação costuma ter seus próprios objetos de entrada, independentes do formato HTTP.
  3. O domínio conhece as mensagens de erro. Ele referencia o projeto Exception para lançar mensagens traduzidas. A alternativa seria lançar códigos de erro e traduzi-los na borda, ao custo de mais código.

Um desvio antigo foi corrigido: o Communication dependia do Domain para expor os enums de status. Hoje o contrato tem enums próprios em português, e a Application converte de um para o outro.


Experimente

  1. Abra o PolicyDesk.Domain.csproj e confira: não há nenhuma referência a EF Core, ASP.NET ou Npgsql.
  2. Tente usar Microsoft.EntityFrameworkCore dentro de Claim.cs. O build falha: a arquitetura é verificada pelo compilador.
  3. Coloque um breakpoint em ApproveClaimUseCase.ExecuteAsync e outro em Claim.Approve, rode a API pela IDE e aprove um sinistro pelo Swagger. Siga a requisição com o depurador e compare com o diagrama acima.

Resumo

  • Arquitetura é sobre deixar mudanças baratas e localizadas.
  • Regra de dependência: o código depende só de coisas mais internas. O domínio não conhece banco nem HTTP.
  • O domínio declara interfaces; a infraestrutura as implementa; a injeção de dependência liga as duas.
  • Cada camada tem um papel claro: regras (Domain), orquestração (Application), banco (Infrastructure), HTTP e montagem (Api).
  • Quase tudo vive por requisição (Scoped), por causa do DbContext.

Para aprofundar

  • Clean Architecture, Robert C. Martin.
  • "The Onion Architecture", série de artigos de Jeffrey Palermo.
  • "Hexagonal Architecture", artigo original de Alistair Cockburn.
  • Próximo artigo sugerido: Modelo de Domínio.

Clone this wiki locally