A evolução do .NET: integrando IA e recursos nativos para dispositivos móveis em aplicações web modernas.
Milan Jovanović publicou recentemente um argumento forte contra a versão prematura de APIs. O ponto central: a maioria das equipes recorre ao v2 cedo demais porque não possui uma estratégia de evolução de contrato. Versionamento é uma ferramenta de compatibilidade, não uma estratégia de design.
O argumento ressoa com nossa equipe de engenharia na Iron Software. Enviamos bibliotecas .NET, o que significa que a superfície pública de nossos produtos é uma API. Cada assinatura de método, cada propriedade, cada comportamento padrão é um contrato que reside dentro de milhares de bases de código dos clientes. Um grande aumento de versão não é um lançamento. É um projeto de migração para todos que estão a jusante.
O que segue é a visão de um desenvolvedor sobre o artigo de Milan a partir da perspectiva de um autor de biblioteca e como as mesmas regras de compatibilidade se aplicam, quer você esteja enviando uma API REST ou um pacote NuGet.
Resumindo
- Versionamento não é uma estratégia de design. É a saída de emergência quando a coexistência falha.
- As mudanças de ruptura se escondem no comportamento, não apenas em URLs ou esquemas.
- As quatro regras de compatibilidade: não remover, não alterar o processamento, não apertar a validação, manter as adições opcionais.
- Uma nova operação é quase sempre mais barata do que uma nova versão.
- A real depreciação requer sinais de tempo de execução e telemetria, não apenas atualizações de documentação.
As regras HTTP se aplicam a APIs de biblioteca também
Milan estrutura a discussão em torno de uma API REST para /orders, mas as mesmas regras se aplicam quando sua API é uma classe pública C# distribuída em um pacote NuGet. O mapeamento é direto:
| Mudança na API REST | Equivalente da biblioteca NuGet |
|---|---|
| Renomeando um campo JSON | Renomeando uma propriedade pública |
| Removendo um endpoint | Removendo um método público |
| Apertando a validação de solicitações | Adicionando um parâmetro não anulável |
| Mudando o comportamento da operação | Mudando o que um método faz internamente |
| Adicionando um campo obrigatório | Adicionando um parâmetro obrigatório no construtor |
Se você já baixou uma versão principal de uma biblioteca .NET popular e gastou meio dia corrigindo APIs renomeadas, você esteve na ponta receptora de uma decisão v2 que provavelmente poderia ter sido tratada de forma aditiva.
O que realmente quebra os consumidores
A lista de Milan é precisa:
- Remover ou renomear campos
- Alterar o significado dos dados existentes
- Apertar a validação de solicitações
- Alterar paginação ou formatos de erro
- Assumir que valores semelhantes a enum são fechados para sempre
O segundo item é o que mais frequentemente pega as equipes de surpresa: mudar o significado dos dados existentes sem mudar sua forma. O JSON parece o mesmo. A assinatura C# parece a mesma. Tudo compila. Nada lança em tempo de execução. Mas o campo agora significa algo diferente, e todos os consumidores que dependiam da semântica antiga estão silenciosamente errados.
Exemplo de Milan:
// Before
{ "total": 100 }
// After
{ "total": { "amount": 100, "currency": "USD" } }
Mesmo nome de campo. Mesmo endpoint. Todo cliente que analisou total como um número agora está quebrado.
O equivalente na biblioteca é mudar o que um método retorna ou como interpreta suas entradas. Um método Save() que anteriormente sobrescrevia e agora adiciona. Um parâmetro Trim cujo padrão muda de true para false. Um método que costumava lançar em entradas inválidas e agora retorna um valor padrão silenciosamente.
As quatro regras de compatibilidade
Milan resume as regras como: não retire nada, não mude as regras de processamento, não torne obrigatórias coisas opcionais, e tudo que você adicionar deve ser opcional. Os quatro princípios valem a pena serem mantidos na frente de qualquer equipe responsável por uma API pública:
- Mantenha campos e comportamentos existentes no lugar.
- Não transforme dados de solicitação opcionais em obrigatórios.
- Não mude o que uma operação existente faz.
- Faça tudo novo ser aditivo e opcional por padrão.
Essas regras se aplicam diretamente ao design de bibliotecas. "Não remova nada" significa não excluir membros públicos. "Não mude as regras de processamento" significa que os métodos existentes devem se comportar como faziam quando foram enviados. "Não torne opcional obrigatório" significa não adicionar parâmetros obrigatórios a um método existente; forneça uma sobrecarga em vez disso. "Adicionativo e opcional" significa que novas funcionalidades pertencem a novos métodos ou parâmetros opcionais com padrões sensatos.
Como isso se desenrola na prática
A maneira mais clara de ilustrar essas regras é com uma decisão real de API, então aqui está uma das nossas.
Alguns lançamentos atrás, o IronPDF precisava suportar um conjunto mais rico de opções de renderização para conversão de HTML para PDF: tamanhos de papel personalizados, margens personalizadas, emulação de mídia CSS, modelos de cabeçalho e rodapé e muito mais. A abordagem direta teria sido alterar o método de renderização existente para aceitar as novas opções. Essa decisão teria quebrado todos os clientes usando a forma simples da API.
Para contexto, a biblioteca é instalada através dos canais padrão de pacotes .NET:
# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdf
# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdf
O pacote IronPdf no NuGet acumulou mais de 18 milhões de downloads, que é parte do motivo pelo qual a estabilidade da API é importante: cada mudança de ruptura reverbera através de tantas integrações.
A abordagem que enviamos em vez disso:
// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
' The original, three-year-old API. Still works. Still unchanged.
Dim renderer As New ChromePdfRenderer()
Dim pdf As PdfDocument = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")
' New rendering options live on an options object, not in the method signature.
renderer = New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print
renderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooter With {.HtmlFragment = "..."}
pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")
Três observações sobre a decisão:
- A assinatura original
RenderHtmlAsPdf(string html)não foi alterada. Os clientes que atualizaram não precisaram modificar uma linha de código. - Novas capacidades vivem em um objeto de opções que os consumidores podem optar por usar. O método não tem novos parâmetros obrigatórios.
- Os padrões em
RenderingOptionsproduzem saída equivalente à API anterior. O comportamento é inalterado para quem não configurar nada.
Essas são as regras 1, 2 e 4 da lista de Milan aplicadas de uma só vez. O produto evoluiu. O contrato não.
A tentação de enviar RenderHtmlAsPdfV2(string html, RenderingOptions options) era real. Teria parecido mais limpo na página de referência da API. Teria custado a cada cliente uma migração. Nós escolhemos outra coisa.
Leitores tolerantes
A outra metade do argumento de Milan "adicionar-não-substituir" é que os consumidores também têm responsabilidade. Um cliente bem-comportado deve ignorar campos que não entende.
No .NET, System.Text.Json ignora propriedades desconhecidas por padrão, o que é o padrão correto. O risco geralmente aparece em dois lugares:
- SDKs gerados com esquemas rígidos que rejeitam campos inesperados
- Testes de contrato que confirmam a igualdade exata de JSON
Ambos transformam a garantia declarada de "ignoramos campos desconhecidos" em uma armadilha. Se seu CI quebra no momento em que o servidor adiciona uma nova propriedade opcional, você não tem compatibilidade para trás. Você tem um detector de regressão disfarçado de política de compatibilidade.
O comportamento é parte do contrato
A seção de Milan sobre DELETE /orders/{id} mudando silenciosamente de exclusão suave para exclusão definitiva é o tratamento mais claro escrito sobre essa questão que já vimos.
A URL é a mesma. O corpo da solicitação é o mesmo. A forma da resposta é a mesma. O que a operação faz no servidor é diferente.
Esta é a categoria mais perigosa de mudança de ruptura porque nada em uma diferença de esquema a captura. A especificação OpenAPI é idêntica. O cliente gerado compila. Os testes de integração passam. E todo consumidor que construiu ferramentas em torno de "pedidos excluídos são recuperáveis" destrói silenciosamente dados em produção.
O equivalente na biblioteca é mudar o que um método faz sem mudar sua assinatura. Exemplos que evitamos explicitamente:
- Um método
Save()que anteriormente limpava de forma síncrona e silenciosamente se torna assíncrono e sem retorno. - Um método OCR que retornava resultados brutos e começa a processá-los posteriormente
- Um leitor de código de barras que lançava em entrada não legível e começa a retornar uma string vazia
Cada um destes é uma ruptura de contrato disfarçada de melhoria. A resposta correta é a mesma de Milan: adicionar um novo método ou opção, deixar o comportamento antigo inalterado e depreciar o caminho antigo apenas quando a telemetria indicar que é seguro fazê-lo.
Estritamente apertando a validação
Esta categoria eventualmente afeta toda equipe. Há duas variantes do mesmo erro:
- Tornar obrigatório um campo opcional existente
- Adicionar um novo campo e marcá-lo como obrigatório desde o primeiro dia
Ambos quebram clientes mais antigos. O caminho do endpoint não se move, mas as solicitações que anteriormente tinham sucesso agora falham em tempo de execução.
A versão na biblioteca deste erro é adicionar um parâmetro obrigatório no construtor ou tornar obrigatório um parâmetro opcional existente. Cada chamador existente quebra em tempo de compilação, o que é preferível à falha em tempo de execução, mas ainda impõe um custo de migração em cada consumidor.
Caminhos mais seguros:
- Aceitar valores ausentes durante uma janela de transição e inferir padrões quando possível.
- Adicionar uma nova sobrecarga ou construtor que requer a forma de entrada mais rica.
- Introduzir uma nova operação ou construtor para o fluxo de trabalho mais rígido.
A regra subjacente é consistente: qualquer coisa adicionada ao contrato deve ser opcional e qualquer coisa previamente opcional deve permanecer opcional. Se requisitos genuinamente mais rigorosos forem necessários, eles pertencem a uma nova operação, não a um aperto da existente.
Uma nova operação é quase sempre mais barata do que uma nova versão
Este é o princípio que mais vale a pena internalizar.
Quando um caso de uso evoluiu genuinamente além do que um endpoint existente suporta de forma clara, o reflexo comum é sobrecarregar o endpoint com flags:
POST /orders?validateOnly=true&includeTaxEstimate=true&reserveInventory=true
Ou, de forma mais disruptiva, declarar a mudança um problema de versionamento e começar a trabalhar no /v2/orders. Ambos estão geralmente errados. A abordagem mais limpa é uma nova operação ao lado da existente:
POST /orders
POST /orders/quote
POST /checkout-sessions
Cada operação tem um contrato claro, permissões distintas, validação independente e seu próprio caminho de evolução. O endpoint original permanece simples. O resto da API não é arrastado para um grande aumento de versão.
No contexto da biblioteca, o equivalente é adicionar um novo método em vez de sobrecarregar um existente com parâmetros opcionais até que se torne ilegível. ExtractText() permanece o extrator de texto simples. ExtractTextWithLayout() torna-se a variante mais rica. ExtractStructuredDocument() torna-se a mais rica. Três métodos com contratos claros são preferíveis a um método com oito parâmetros opcionais.
Descontinuar deliberadamente
Esta é a metade do gerenciamento de mudanças de API que a maioria das equipes pula, e é a metade que determina se a estratégia funciona.
A depreciação real não é uma nota em um log de alterações. Envolve quatro etapas:
- Marque o campo ou endpoint como obsoleto na descrição OpenAPI (ou com o atributo
[Obsolete]no mundo .NET). - Sinalize a depreciação em tempo de execução para que o tráfego ao vivo a exponha.
- Vincule a um guia real de migração.
- Meça o uso com telemetria para determinar quando a remoção é segura.
Para APIs HTTP, o sinal de tempo de execução é direto:
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.example.com/migrations/orders-total>; rel="deprecation"
Para bibliotecas .NET, o equivalente é um [Obsoleto("Use o NewMethod em vez disso. Isso será removido na v2026.x", DiagnosticId = "IRON001")] attribute paired with a UrlFormat apontando para uma página de migração. O aviso do compilador aparece na saída de build de cada consumidor, o identificador de diagnóstico permite supressão deliberada, e o link dá aos consumidores um caminho de migração documentado.
A etapa de telemetria é inegociável. Sem saber quais clientes ainda dependem do método depreciado, a remoção se torna uma adivinhação. O resultado é uma remoção prematura que quebra integrações ativas ou um custo de carregamento indefinido que derrota o propósito da depreciação.
Quando o versionamento é a escolha certa
Milan não é contra o versionamento, nem nós somos. O versionamento é apropriado quando:
- As antigas e novas semânticas genuinamente não podem coexistir
- O modelo de recurso mudou fundamentalmente
- As regras de compatibilidade forçariam um contrato que ninguém pode entender
O ponto não é evitar o versionamento completamente. O ponto é alcançá-lo porque a coexistência falhou, não porque foi a primeira ideia na mesa.
Quando o versionamento é necessário, deve ser acompanhado de um processo real de depreciação. O trabalho difícil não é enviar v2. O trabalho difícil é tirar os consumidores de v1.
A regra de decisão
A estrutura de Milan é a certa a se aplicar:
- Posso adicionar em vez de substituir?
- Contratos antigos e novos podem coexistir durante uma janela de migração?
- Posso introduzir uma nova operação em vez de modificar uma antiga?
- Posso depreciar a forma antiga com documentação, cabeçalhos e telemetria?
Se a resposta for sim para todos os quatro, uma nova versão provavelmente não é necessária. Se a resposta for não, e os dois mundos genuinamente não puderem coexistir, versionar deliberadamente.
Projete contratos para evoluir. Trate os consumidores como integrações de longa duração em vez de código de hoje. Reserve o versionamento para os casos em que a compatibilidade foi genuinamente esgotada.
Para o artigo completo, incluindo exemplos mais longos, leia o post original de Milan.
Ao selecionar uma biblioteca .NET para depender, a pergunta que vale a pena fazer é aquela em que o post de Milan se baseia: esta biblioteca ainda se parecerá com a API que integrei em três anos?
Essa é a pergunta que nos esforçamos para responder a cada lançamento. As chamadas simples de 2020 ainda funcionam. Novas capacidades estão ao lado delas, opcionais e aditivas. Sem migrações de versão maior forçadas.
Se essa abordagem de design de biblioteca corresponde ao que você precisa, inicie um teste gratuito de 30 dias e revise a referência da API por si mesmo. O início rápido de cinco minutos orienta sobre instalação, ativação de licença e um primeiro PDF renderizado. O próprio pacote está a um comando de qualquer projeto .NET:
Para ambientes onde o NuGet não é o caminho preferido, o download direto fornece o arquivo DLL e o instalador do Windows.