For future Claude
Páginas MedTrans devolviam 500 com InvalidCastException: DBNull -> Boolean: colunas booleanas nullable no MySQL legado mapeadas para bool não-anulável no .NET. Caiu o smoke de 24/24 para 14/24; resolvido a 22/24 (as 2 restantes não são deste defeito). Não foi causado pelo trabalho de Grafana v6 — foi revelado por ele.
Duas lições que valem mais que o fix:
- Value converters da EF Core não resolvem isto. A EF nunca invoca o converter para NULL, e a nulidade da coluna é decidida pelo tipo CLR da propriedade, não pelo tipo do provider do converter. A propriedade tem de ser
bool?. - O
deploy.pymentia. Oensure_publishreutilizava a pastapublish/antiga mesmo sem--skip-publish, o Docker marcavaCOPY publish/web/comoCACHEDe o deploy diziaSUCCESScom binários velhos. Duas tentativas de fix pareceram não funcionar por isto. Já corrigido — se um fix "não faz efeito", confirmar que oCOPY publish/não vemCACHED.
Diagnóstico: python _find_null_bools.py. Relacionado: AuthZ-Bugs-2026-08-01 · Homologacao · Observability · Base-de-Dados.
Defeito — DBNull → Boolean (2026-08-19)
Sintoma
| Perfil | Rota | Resultado |
|---|---|---|
| Supervisor | /MedTrans/Jobs, Jobs/Detail, Jobs/Edit, Jobs/Workflow, Manage/Settings, Download/* |
500 errpage |
| Client / Administrator | BUG-01/02/03/05/07 em _verify_bugs.py |
got=crash |
System.InvalidCastException: Unable to cast object of type 'System.DBNull' to type 'System.Boolean'.
at MySqlConnector.Core.Row.GetBoolean(Int32 ordinal)
at ...BufferedDataRecord.ReadBool(DbDataReader reader, Int32 ordinal, ReaderColumn column)
Causa raiz
Duas camadas.
1. Materialização acidental da entidade Client. JobDashboardService.MapListAsync projeta
(c.User!.FirstName ?? c.FirstName), mas Client.FirstName/LastName são Ignore no modelo MySQL
(os nomes vivem em users). A projeção deixa de ser traduzível, a EF materializa a entidade Client
inteira e passa a ler as 28 colunas de user_client, incluindo todas as flags:
SELECT `u`.`user_id`, `u1`.`firstname`, ..., `u`.`send_copy_of_final_documents`,
`u`.`requires_payment_for_android`, ...
FROM `user_client` AS `u` INNER JOIN (...) AS `u1` ON `u`.`user_id` = `u1`.`Id`
WHERE `u`.`user_id` IN (748, 757, 749)
2. Flags nullable lidas para bool não-anulável. Das 11 colunas com NULL na base, só 5 estão
mapeadas no .NET — todas em user_client:
| Coluna | Linhas NULL |
|---|---|
user_client.send_copy_of_final_documents |
721 / 721 |
user_client.requires_payment_for_lexacom |
48 |
user_client.requires_payment_for_winscribe |
16 |
user_client.requires_payment_for_android |
16 |
user_client.requires_payment_for_speech_live |
9 |
As restantes (users.is_uploading 674, organizations.isDefaultTo24HoursJob 27,
jobs.big_hand_not_transferred 6, jobs.media_hawk_not_transferred 6, company.get_job_from_mail 2,
company.subtract_from_linecount 1) não estão mapeadas — confirmado no SQL emitido, não aparecem
em nenhum SELECT. Não entram em nenhuma query.
LegacyMySqlSchemaPatches.cs declara requires_payment_for_* como TINYINT(1) NOT NULL DEFAULT 0,
mas as linhas existentes ficaram a NULL — o patch não fez backfill.
Porque a opção "converter EF" foi abandonada
Foi a primeira escolha, e foi testada a fundo: converter ValueConverter<bool, bool?> aplicado a
todas as flags do modelo legado (loop sobre modelBuilder.Model.GetEntityTypes()), confirmado
presente no DLL dentro do container, e a exceção manteve-se idêntica em ReadBool.
Razão: a EF Core nunca invoca o converter para NULL, e a nulidade da coluna vem de
property.IsNullable, derivado do tipo CLR (bool), não do tipo do provider do converter. Com a
coluna considerada NOT NULL, a EF emite ReaderColumn<bool> → GetBoolean → DBNull → crash.
O LegacyBoolConverters.cs foi removido para não deixar no código uma correção que não funciona.
Fix aplicado
As 12 flags passaram a bool? no domínio, com default false:
Client.SendCopyOfFinalDocuments(D2U.Domain/Users/User.cs)- os 11
RequiresPaymentFor*(D2U.Domain/Users/ClientBillingPreferences.cs)
Leitura com ?? false, igual à semântica do Java (getRequiresPaymentForWinscribe devolve
FALSE quando o campo é null). PaymentService e LegacyContractorAutoAssignService já usavam
prefs?.X ?? false e não precisaram de mudar; só WorkerServices (cópia BigHand) e
MedTransChromeFilter (mostrar créditos) precisaram de coalescer.
O schema PostgreSQL normalizado mantém-se NOT NULL: IsRequired() nas configurações PG-only
(ClientConfiguration, ClientBillingPreferencesConfiguration) mais o default false no domínio,
para o seeder não gravar NULL. Zero escritas na base partilhada com o Java.
Resultado
| Métrica | Antes | Depois |
|---|---|---|
_verify_bugs.py |
14/24 | 22/24 |
| Supervisor — rotas OK | 15/33 | 22/33 |
action_crash (todos os perfis) |
1 | 0 |
| Exceções no log da web | 96 | 0 |
As 2 falhas que restam não são deste defeito: BUG-06 DupSession (mensagem
index.login.4 de sessão duplicada, ainda não unificada entre Java e d2u_active_sessions) e
BUG-07 Split Supervisor (paridade authZ de Split). Ver AuthZ-Bugs-2026-08-01.
Risco residual
Quatro colunas de user_client são nullable mas hoje têm 0 NULLs: is_activated, on_hold,
notify_by_email, notify_by_email_job_created. Ficaram deliberadamente como bool porque
IsActivated é a porta de activação do login do cliente e ?? false negaria acesso — mudá-las exige
primeiro confirmar o default do Java. Se o Java começar a gravar NULL nestas, o mesmo 500 volta;
python _find_null_bools.py deteta.