.NET 10 e as mudanças no TimeSpan.FromMilliseconds

O TimeSpan.FromMilliseconds ganhou uma nova sobrecarga no .NET 10, lançado em novembro de 2025, e essa mudança resolve um problema sutil, mas significativo, em expressões LINQ. À primeira vista parece um detalhe, mas ele afeta quem escreve expression trees, consultas com IQueryable e bibliotecas que analisam código em tempo de execução.

Neste artigo, você vai entender de onde veio o problema, por que o compilador recusava uma chamada aparentemente inocente e o que a nova assinatura muda no seu código. Além disso, vamos ver exemplos antes e depois da correção.

O que é o TimeSpan.FromMilliseconds

O TimeSpan é a struct do .NET que representa um intervalo de tempo, como 30 segundos ou duas horas. Ele aparece em timeouts, políticas de retry, caches e em praticamente qualquer código que lida com duração.

Para criar um TimeSpan, a forma mais comum é usar os métodos de fábrica From*. Por exemplo, TimeSpan.FromSeconds(30) cria um intervalo de 30 segundos.

Nesse grupo, o TimeSpan.FromMilliseconds cria um intervalo a partir de uma quantidade de milissegundos. Ou seja, TimeSpan.FromMilliseconds(1500) representa um segundo e meio.

var timeout = TimeSpan.FromMilliseconds(1500);
Console.WriteLine(timeout); // 00:00:01.5000000

Por muitos anos, esse método teve apenas uma assinatura, que recebia um double. Contudo, isso mudou no .NET 9, e é aí que a história começa.

A origem: sobrecargas inteiras no .NET 9

No .NET 9, a equipe do runtime adicionou sobrecargas que recebem inteiros em FromDays, FromHours, FromMinutes, FromSeconds, FromMilliseconds e FromMicroseconds. O objetivo era resolver um problema antigo de precisão.

O problema da precisão do double

O tipo double usa ponto flutuante binário e, por isso, carrega uma imprecisão natural. Segundo a documentação de novidades das bibliotecas do .NET 9, TimeSpan.FromSeconds(101.832) não representa exatamente 101 segundos e 832 milissegundos.

Na prática, o resultado fica em algo como 101 segundos e 831,99999 milissegundos. Assim, o valor impresso termina em .8319999, e não em .8320000.

TimeSpan timeSpan1 = TimeSpan.FromSeconds(value: 101.832);
Console.WriteLine(timeSpan1); // 00:01:41.8319999

TimeSpan timeSpan2 = TimeSpan.FromSeconds(seconds: 101, milliseconds: 832);
Console.WriteLine(timeSpan2); // 00:01:41.8320000

Com as sobrecargas inteiras, você informa cada parte do intervalo separadamente. Dessa forma, o valor final fica exato.

No caso do TimeSpan.FromMilliseconds, o .NET 9 trouxe a seguinte assinatura nova:

public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);

O segundo parâmetro era opcional, com valor padrão zero. Portanto, a chamada TimeSpan.FromMilliseconds(1000) passou a escolher essa versão inteira, e não mais a versão com double.

Motivação por trás da mudança no TimeSpan.FromMilliseconds

Embora essa implementação funcionasse bem na maioria dos casos, ela introduziu um problema quando usada dentro de expressões LINQ. O motivo é que expression trees não suportam chamadas que dependem de parâmetros opcionais.

Isso significa que a linha abaixo, aparentemente inofensiva, gerava erro de compilação no .NET 9:

Expression<Action> action = () => TimeSpan.FromMilliseconds(1000);

O compilador responde com o erro CS0854, cuja mensagem diz que uma expression tree não pode conter uma chamada que usa argumentos opcionais. Em outras palavras, o código compila como lambda comum, mas falha como Expression<T>.

Por que expression trees rejeitam parâmetros opcionais

Uma expression tree é uma representação do código como dados. Em vez de gerar IL executável, o compilador monta uma árvore de objetos que descreve a chamada, os argumentos e os operadores.

Bibliotecas como o Entity Framework Core leem essa árvore e a traduzem para outra linguagem, como SQL. Por isso, a árvore precisa ser explícita e completa.

Quando um método tem parâmetro opcional, o compilador normalmente preenche o valor padrão no ponto da chamada. Contudo, o C# historicamente proíbe esse preenchimento dentro de expression trees e emite o CS0854.

O mesmo vale para argumentos nomeados, que geram o erro CS0853. Assim, qualquer API pública com parâmetro opcional fica difícil de usar nesse contexto.

Como a resolução de sobrecarga escolhe o método

O detalhe mais curioso está na resolução de sobrecarga. Ao chamar TimeSpan.FromMilliseconds(1000), o literal 1000 é um int.

No .NET 9, havia duas candidatas: FromMilliseconds(double) e FromMilliseconds(long, long = 0). Como a conversão de int para long é considerada melhor do que a conversão para double, o compilador escolhia a versão inteira.

Consequentemente, a chamada passava a depender do parâmetro opcional microseconds. Ou seja, um código que funcionava em versões anteriores do .NET quebrava ao migrar para o .NET 9, sem nenhuma alteração do desenvolvedor.

Esse é exatamente o tipo de quebra de compatibilidade de código fonte que a equipe do .NET tenta evitar. Por isso, a correção veio logo no ciclo seguinte.

A nova assinatura do TimeSpan.FromMilliseconds no .NET 10

Para resolver o problema, o .NET 10 adicionou uma sobrecarga que recebe um único parâmetro e removeu a opcionalidade do segundo parâmetro da versão existente. A documentação de novidades das bibliotecas do .NET 10 descreve essa alteração na seção de data e hora.

public readonly struct TimeSpan
{
    // Agora o segundo parâmetro é obrigatório
    public static TimeSpan FromMilliseconds(long milliseconds, long microseconds);

    // Nova sobrecarga com um único parâmetro
    public static TimeSpan FromMilliseconds(long milliseconds);

    // Sobrecarga original, que continua existindo
    public static TimeSpan FromMilliseconds(double value);
}

Com isso, a chamada TimeSpan.FromMilliseconds(1000) passa a escolher a nova versão FromMilliseconds(long). Como ela não tem parâmetro opcional, a expression tree volta a compilar.

Vale notar que o TimeSpan.FromMilliseconds era um caso isolado. Por exemplo, o FromSeconds já tinha uma sobrecarga FromSeconds(long) desde o .NET 9, enquanto o método de milissegundos ficou sem ela.

A própria documentação do .NET 10 reconhece isso. Segundo o texto oficial, o método com dois parâmetros foi introduzido anteriormente sem uma sobrecarga de parâmetro único.

Benefícios da nova implementação do TimeSpan.FromMilliseconds

Essa mudança traz alguns benefícios importantes, mesmo sendo pequena. Primeiro, ela devolve a compatibilidade com LINQ e expressões lambda.

Com a nova sobrecarga, você usa TimeSpan.FromMilliseconds(1000) em expressões LINQ sem enfrentar erros de compilação. Além disso, o comportamento volta a ser o mesmo das versões anteriores ao .NET 9.

Em segundo lugar, o código fica mais claro e expressivo. Afinal, cada assinatura agora tem um propósito único: uma recebe milissegundos, outra recebe milissegundos e microssegundos, e a terceira aceita frações com double.

Por fim, a mudança evita ambiguidades futuras. O uso de parâmetros opcionais em métodos públicos pode causar problemas inesperados ao longo do tempo, especialmente em APIs muito usadas em expressões compiláveis.

Essa última lição vale para quem escreve bibliotecas. Ao adicionar um parâmetro opcional a uma API pública, você pode quebrar chamadas que já existiam dentro de expression trees.

Exemplo prático com TimeSpan.FromMilliseconds

Vamos comparar a utilização antes e depois dessa melhoria. Os exemplos usam System.Linq.Expressions, o namespace que define as expression trees.

Código antes da mudança

No .NET 9, a chamada direta funcionava normalmente, mas a versão em expression tree falhava:

using System.Linq.Expressions;

// Funciona normalmente
var timeSpan1 = TimeSpan.FromMilliseconds(1000);

// Erro CS0854 ao usar em expression trees no .NET 9
Expression<Func<TimeSpan>> expression = () => TimeSpan.FromMilliseconds(1000);

Para contornar o erro, era preciso informar o segundo argumento ou forçar a versão com double. Ambas as saídas funcionavam, mas deixavam o código menos natural.

// Contorno 1: informar os microssegundos explicitamente
Expression<Func<TimeSpan>> withMicroseconds = () => TimeSpan.FromMilliseconds(1000, 0);

// Contorno 2: forçar a sobrecarga com double
Expression<Func<TimeSpan>> withDouble = () => TimeSpan.FromMilliseconds(1000d);

Código após a mudança no .NET 10

No .NET 10, as duas formas funcionam sem nenhum ajuste:

using System.Linq.Expressions;

// Continua funcionando normalmente
var timeSpan1 = TimeSpan.FromMilliseconds(1000);

// Agora funciona corretamente em expression trees
Expression<Func<TimeSpan>> expression = () => TimeSpan.FromMilliseconds(1000);

var compiled = expression.Compile();
Console.WriteLine(compiled()); // 00:00:01

Repare que a expressão pode ser compilada e executada como qualquer delegate. Assim, você confirma que a árvore chama a nova sobrecarga com um único parâmetro.

Uso do TimeSpan.FromMilliseconds em consultas

O cenário mais comum de expression trees no dia a dia é o IQueryable. Quando você escreve um Where sobre um IQueryable, o compilador gera uma expression tree, e não um delegate.

// O filtro vira uma expression tree porque Jobs é IQueryable
var slowJobs = dbContext.Jobs
    .Where(job => job.Duration > TimeSpan.FromMilliseconds(500))
    .ToList();

No .NET 9, esse código parava no CS0854 ainda na compilação. Já no .NET 10, ele compila, e a tradução final depende do provedor de dados usado.

Por isso, mesmo quem nunca escreveu Expression<T> à mão pode ter esbarrado nesse problema. Afinal, filtros com TimeSpan.FromMilliseconds em consultas são relativamente comuns.

Se você quer revisar outros recursos do LINQ, o artigo sobre como utilizar o let no LINQ mostra como deixar consultas mais legíveis.

Como inspecionar o TimeSpan.FromMilliseconds na expression tree

Uma forma simples de confirmar qual sobrecarga o compilador escolheu é inspecionar a própria árvore. Afinal, a expression tree guarda o método chamado como um objeto MethodInfo.

using System.Linq.Expressions;

Expression<Func<TimeSpan>> expression = () => TimeSpan.FromMilliseconds(1000);

// O corpo da lambda é uma chamada de método
var call = (MethodCallExpression)expression.Body;

Console.WriteLine(call.Method);
// System.TimeSpan FromMilliseconds(Int64)

Console.WriteLine(call.Method.GetParameters().Length);
// 1

No .NET 10, o resultado mostra a sobrecarga com um único parâmetro Int64. Com isso, você tem uma prova concreta de que a árvore não depende mais de argumentos opcionais.

Essa técnica também ajuda a depurar outros erros de expression trees. Por exemplo, quando um provedor LINQ não consegue traduzir uma chamada, inspecionar o MethodCallExpression revela exatamente qual método chegou até ele.

Qual sobrecarga do TimeSpan.FromMilliseconds escolher

Depois da mudança, o TimeSpan.FromMilliseconds passa a ter três versões. A tabela abaixo resume quando usar cada uma delas.

Sobrecarga Quando usar
FromMilliseconds(long) Valores inteiros de milissegundos, como timeouts e intervalos de retry.
FromMilliseconds(long, long) Quando você precisa somar microssegundos sem perder precisão.
FromMilliseconds(double) Valores fracionados que chegam de cálculos ou de configurações numéricas.

Na maior parte do tempo, a primeira opção resolve. Além disso, ela é a escolhida automaticamente quando você passa um literal inteiro.

A versão com double continua útil quando o valor vem de uma conta, como uma média de tempos de resposta. Nesse caso, lembre que o resultado tem precisão limitada ao milissegundo mais próximo, conforme a documentação do método.

Impacto na compatibilidade do seu código

Para a maioria dos projetos que usam o TimeSpan.FromMilliseconds, a mudança é transparente e não exige nenhuma ação manual. Ao recompilar com o SDK do .NET 10, as chamadas com um único argumento passam a usar a nova sobrecarga automaticamente.

Chamadas que já informavam os dois argumentos continuam iguais, porque a assinatura FromMilliseconds(long, long) segue existindo. Dessa forma, só muda a obrigatoriedade do segundo parâmetro.

A sobrecarga com double também permanece disponível. Portanto, se você precisa de frações de milissegundo, como TimeSpan.FromMilliseconds(1.5), nada muda.

Uma boa prática é preferir as versões inteiras sempre que o valor não tiver casas decimais. Assim, você evita a imprecisão do ponto flutuante e deixa a intenção explícita.

// Valor exato com a sobrecarga inteira
var precise = TimeSpan.FromMilliseconds(250);

// Milissegundos e microssegundos sem perda de precisão
var veryPrecise = TimeSpan.FromMilliseconds(250, 500);

Projetos com multi-targeting merecem um cuidado extra. Se a sua biblioteca compila para net8.0, net9.0 e net10.0 ao mesmo tempo, cada alvo enxerga um conjunto diferente de sobrecargas.

No net8.0, existe apenas a versão com double, então a expression tree compila sem problemas. No net9.0, porém, a chamada com inteiro cai na versão com parâmetro opcional e dispara o CS0854.

Já no net10.0, a nova sobrecarga resolve a questão. Portanto, enquanto o alvo net9.0 existir no projeto, mantenha um dos contornos mostrados acima nas expression trees.

<PropertyGroup>
  <!-- O alvo net9.0 ainda exige o contorno nas expression trees -->
  <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

Para entender melhor como o .NET lida com tempo, vale também ler o artigo sobre DateTime no C# e .NET. Ele complementa o tema com o lado das datas.

E a linguagem C#, também muda?

O caso do TimeSpan.FromMilliseconds reacendeu uma discussão na própria linguagem. Afinal, a restrição a argumentos opcionais em expression trees existe há muitos anos.

A equipe do C# publicou uma proposta de especificação para argumentos opcionais e nomeados em expression trees. O texto cita o risco de quebra quando uma nova sobrecarga com parâmetro opcional se torna aplicável a uma chamada existente.

A proposta sugere remover os erros CS0854 e CS0853 e deixar o compilador preencher os argumentos normalmente. Inclusive, ela reconhece que não está claro por que a restrição foi criada originalmente.

No entanto, a correção do TimeSpan.FromMilliseconds não depende dessa proposta. Ela acontece na biblioteca, por meio da nova sobrecarga, e funciona independentemente da versão da linguagem.

Conclusão

Essa mudança pode parecer pequena à primeira vista, mas resolve um problema que causava dor de cabeça em cenários avançados do C#. Afinal, ela devolve o comportamento esperado a uma chamada que todo desenvolvedor .NET já escreveu.

A história mostra bem como uma melhoria pode gerar efeitos colaterais. As sobrecargas inteiras do .NET 9 corrigiram a precisão, mas o parâmetro opcional do TimeSpan.FromMilliseconds quebrou as expression trees.

O .NET 10 continua trazendo refinamentos para tornar o desenvolvimento mais previsível e amigável. Com a nova sobrecarga, o TimeSpan.FromMilliseconds volta a funcionar em lambdas, LINQ e IQueryable, sem armadilhas de compilação.

Se você estava usando contornos como FromMilliseconds(1000, 0) ou FromMilliseconds(1000d), já pode removê-los ao migrar para o .NET 10. Assim, o código fica mais limpo e mais fácil de manter.

FAQ

O que mudou no TimeSpan.FromMilliseconds no .NET 10?

O método ganhou a sobrecarga FromMilliseconds(long), com um único parâmetro. Além disso, o segundo parâmetro de FromMilliseconds(long, long) deixou de ser opcional.

Por que o TimeSpan.FromMilliseconds dava erro em expressões LINQ?

No .NET 9, a chamada com um inteiro escolhia uma sobrecarga com parâmetro opcional. Expression trees não aceitam argumentos opcionais, o que gerava o erro CS0854.

Preciso alterar meu código ao migrar para o .NET 10?

Normalmente não, porque a recompilação já escolhe a nova sobrecarga. Você só pode remover contornos que tenha criado para o erro.

A sobrecarga com double foi removida?

Não. FromMilliseconds(double) continua disponível para valores com frações de milissegundo.

Quando usar a sobrecarga inteira em vez da double?

Use a versão inteira sempre que o valor não tiver casas decimais. Ela evita a imprecisão natural do ponto flutuante.