C# 14 e o novo modelo de Extension Members
Os extension members são a principal novidade do C# 14 para quem escreve bibliotecas e APIs reutilizáveis, porque ampliam o conceito de métodos de extensão que os desenvolvedores C# já conhecem. Com o C# 14 e o .NET 10, a linguagem deu mais um passo em direção a uma extensibilidade elegante.
Neste artigo, vamos relembrar como funcionam os extension methods tradicionais, entender a nova sintaxe com blocos extension e ver exemplos práticos. Além disso, vamos discutir compatibilidade, regras de resolução e cuidados no dia a dia.
Extension methods antes do C# 14
Até o C# 13, os extension methods ficavam dentro de uma classe estática e também precisavam ser estáticos. Além disso, o primeiro parâmetro usava a palavra-chave this para indicar o tipo estendido.
Por exemplo, para adicionar um comportamento personalizado ao tipo string, criamos um método como public static bool IsNullOrEmpty(this string value). Assim, podemos chamar esse método como se fosse parte da instância: "text".IsNullOrEmpty().
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
}
// Uso:
string name = "balta";
bool isEmpty = name.IsNullOrEmpty(); // false
Essa abordagem aumenta a expressividade do código sem alterar os tipos originais. Não por acaso, toda a LINQ se apoia nela, com métodos como Where, Select e OrderBy estendendo IEnumerable<T>.
Se você quer revisar o modelo clássico em detalhes, o artigo sobre extension methods no C# cobre a criação e o uso. Para organizar, testar e reutilizar extensões, veja também o guia de criação, segmentação e testes de extension methods.
As limitações do modelo antigo
O modelo com this funciona muito bem para métodos de instância. No entanto, ele para por aí.
Não havia como criar uma propriedade de extensão, por exemplo. Por isso, bibliotecas acabavam expondo métodos como IsEmpty() quando, semanticamente, o ideal seria uma propriedade IsEmpty.
Também não era possível adicionar membros estáticos a um tipo existente. Ou seja, não dava para escrever algo como IEnumerable<int>.Identity e fazer a chamada parecer nativa do tipo.
Outro ponto incômodo era a repetição. Em uma classe com dez extensões para o mesmo tipo, o parâmetro this IEnumerable<T> source aparecia dez vezes, junto com o parâmetro genérico de cada método.
O que mudou com os extension members
O C# 14 introduz os extension blocks, blocos declarados com a palavra-chave extension. Dentro deles, você declara vários extension members para o mesmo tipo de uma só vez.
Com essa sintaxe, além dos métodos, agora também é possível declarar:
- Propriedades de extensão (extension properties).
- Membros de extensão estáticos, que se comportam como membros estáticos do próprio tipo estendido.
- Operadores definidos pelo usuário, implementados como membros estáticos de extensão.
Essas melhorias permitem escrever APIs mais expressivas e reutilizáveis. Tudo isso, mais uma vez, sem modificar os tipos originais.
Vale um esclarecimento importante. Os primeiros anúncios do recurso citavam indexadores de extensão, mas a documentação oficial registra que eles só chegam no C# 15.
Anatomia de um bloco extension
Um bloco extension sempre fica dentro de uma classe estática, não genérica e não aninhada. Essa é a mesma regra que já valia para os extension methods tradicionais.
Na declaração do bloco, você informa o receiver, isto é, o tipo estendido e, opcionalmente, um nome para a instância. Em seguida, declara os membros normalmente, como faria em uma classe comum.
namespace CustomExtensionMembers;
public static class StringExtensions
{
// O receiver é a instância de string chamada value
extension(string value)
{
// Método de extensão sem o parâmetro this
public int WordCount() =>
value.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
// Propriedade de extensão
public bool IsBlank => string.IsNullOrWhiteSpace(value);
}
}
Repare que o nome value fica disponível em todos os membros de instância do bloco. Dessa forma, você não repete o parâmetro em cada método.
Uma mesma classe pode conter quantos blocos de extension members quiser. Além disso, blocos diferentes não precisam estender o mesmo tipo nem usar o mesmo nome de receiver.
Exemplo prático de extension members
A documentação do C# 14 traz um exemplo baseado na própria classe Enumerable do .NET. Ele mostra, em um único arquivo, os dois tipos de bloco.
public static class Enumerable
{
// Bloco de extensão com instância
extension<TSource>(IEnumerable<TSource> source)
{
// Propriedade de extensão
public bool IsEmpty => !source.Any();
// Método de extensão
public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
}
// Bloco de extensão estática, apenas com o tipo
extension<TSource>(IEnumerable<TSource>)
{
// Método estático de extensão
public static IEnumerable<TSource> Combine(IEnumerable<TSource> first, IEnumerable<TSource> second) { ... }
// Propriedade estática de extensão
public static IEnumerable<TSource> Identity => Enumerable.Empty<TSource>();
// Operador definido pelo usuário
public static IEnumerable<TSource> operator +(IEnumerable<TSource> left, IEnumerable<TSource> right)
=> left.Concat(right);
}
}
O primeiro bloco declara um receiver com nome, source, e por isso contém membros de instância. Já o segundo informa apenas o tipo, porque todos os seus membros são estáticos.
A versão original deste artigo incluía também um indexador, public TSource this[int index], no primeiro bloco. Como vimos, essa sintaxe só funciona a partir do C# 15, então a deixamos de fora do exemplo.
Como usar extension members
Do lado de quem consome a API, nada muda em relação a um membro comum. Em resumo, as regras são simples:
- Os membros do primeiro bloco se comportam como membros de instância. Exemplo:
sequence.IsEmpty. - Os membros do segundo bloco funcionam como membros estáticos do tipo. Exemplo:
IEnumerable<int>.Identity.
IEnumerable<int> numbers = new List<int> { 1, 2, 3 };
// Usando os membros de instância
bool isEmpty = numbers.IsEmpty; // false
// Usando os membros estáticos
var combined = IEnumerable<int>.Combine(new[] { 1, 2 }, new[] { 3, 4 }); // [1, 2, 3, 4]
var identity = IEnumerable<int>.Identity; // Enumerable.Empty<int>()
// Usando o operador de extensão
var merged = numbers + Enumerable.Range(4, 2); // [1, 2, 3, 4, 5]
Assim como nos extension methods, os membros só ficam visíveis quando você importa o namespace com using. Portanto, a organização dos namespaces continua importante.
Propriedades de extensão
Entre os extension members, as propriedades de extensão talvez sejam o ganho mais visível no dia a dia. Com elas, informações derivadas de um objeto finalmente podem ter a forma de propriedade.
Imagine uma entidade de domínio com nome e sobrenome. Antes, a camada de apresentação criava um método FullName(); agora, ela pode declarar uma propriedade:
public class Customer
{
public int Id { get; set; }
public required string FirstName { get; set; }
public required string LastName { get; set; }
}
public static class CustomerExtensions
{
extension(Customer customer)
{
// Propriedade calculada, sem estado próprio
public string FullName => $"{customer.FirstName} {customer.LastName}";
}
}
// Uso
var customer = new Customer { FirstName = "Jane", LastName = "Doe" };
Console.WriteLine(customer.FullName); // Jane Doe
Repare que a propriedade é calculada a partir do receiver. Afinal, uma extensão não adiciona campos ao tipo estendido, então ela não guarda estado próprio.
Esse padrão encaixa bem em arquiteturas em camadas. Na prática, cada camada adiciona os comportamentos de que precisa, enquanto as entidades continuam enxutas.
Membros estáticos e operadores
Os extension members estáticos resolvem um problema antigo: criar métodos de fábrica ou valores padrão que parecem pertencer ao tipo. Para isso, declare um bloco sem nome de receiver.
public static class SequenceExtensions
{
extension(IEnumerable<int>)
{
// Gera uma sequência a partir de um valor inicial e de um incremento
public static IEnumerable<int> Generate(int low, int count, int increment)
{
for (int i = 0; i < count; i++)
yield return low + (i * increment);
}
}
}
// Uso, como se Generate fosse membro estático de IEnumerable<int>
var sequence = IEnumerable<int>.Generate(5, 10, 2);
Os operadores seguem a mesma lógica. Você os declara como membros estáticos dentro do bloco, e o compilador os aplica como se fossem operadores definidos no próprio tipo.
Mesmo assim, use operadores de extensão com moderação. Por exemplo, um + entre sequências pode confundir quem lê o código sem conhecer a extensão.
Extension members genéricos
Os blocos de extension members aceitam tipos genéricos abertos ou fechados, inclusive com restrições. A regra para posicionar os parâmetros de tipo é simples.
Se o parâmetro de tipo aparece no receiver, ele vai na declaração do extension. Por outro lado, se o membro precisa de um tipo adicional, esse parâmetro vai no próprio membro.
public static class GenericExtensions
{
extension<TReceiver>(IEnumerable<TReceiver> source)
where TReceiver : IEquatable<TReceiver>
{
// Usa apenas o parâmetro de tipo do receiver
public IEnumerable<TReceiver> ValuesEqualTo(TReceiver target)
=> source.Where(item => item.Equals(target));
// Declara um parâmetro de tipo extra, TArg, no próprio membro
public IEnumerable<TReceiver> AppendConverted<TArg>(
IEnumerable<TArg> items,
Func<TArg, TReceiver> converter)
=> source.Concat(items.Select(converter));
}
}
Você não pode repetir o mesmo parâmetro de tipo nos dois lugares. Assim, a declaração fica mais limpa do que no modelo antigo, em que cada método repetia <T> e this IEnumerable<T>.
Documentando extension members
Bibliotecas públicas precisam de documentação XML, e o novo modelo também cuida disso. Nesse caso, você documenta o bloco extension com summary e param, descrevendo o tipo estendido e o receiver.
/// <summary>
/// Extensões para sequências de inteiros.
/// </summary>
public static class NumericSequences
{
/// <summary>
/// Define extensões para sequências de inteiros.
/// </summary>
/// <param name="sequence">A sequência usada como receiver.</param>
extension(IEnumerable<int> sequence)
{
/// <summary>
/// Soma um valor a cada elemento da sequência.
/// </summary>
/// <param name="operand">O valor a somar.</param>
public IEnumerable<int> AddValue(int operand)
{
foreach (var item in sequence)
yield return item + operand;
}
}
}
O compilador copia o nó do bloco para cada membro gerado. Assim, o IntelliSense mostra a descrição do receiver em todos os extension members, sem que você repita o comentário.
Migrando uma classe de extensões
Para sentir a diferença na prática, vamos migrar uma classe típica de projeto. Primeiro, observe a versão clássica, com três métodos para o mesmo tipo:
public static class OrderExtensions
{
public static bool IsPaid(this Order order) => order.PaidAt is not null;
public static decimal TotalWithTax(this Order order, decimal rate)
=> order.Total * (1 + rate);
public static bool IsLate(this Order order, DateTime now)
=> order.DueDate < now && order.PaidAt is null;
}
Em seguida, veja a mesma classe com extension members. Repare que IsPaid deixa de ser um método e passa a ser uma propriedade:
public static class OrderExtensions
{
extension(Order order)
{
// Agora é uma propriedade, e não um método
public bool IsPaid => order.PaidAt is not null;
public decimal TotalWithTax(decimal rate) => order.Total * (1 + rate);
public bool IsLate(DateTime now) => order.DueDate < now && !order.IsPaid;
}
}
O receiver aparece uma única vez, e os membros ficam mais próximos de uma classe comum. Além disso, um membro do bloco pode usar outro, como IsLate faz com IsPaid.
Atenção a um detalhe: transformar IsPaid() em propriedade muda a forma de chamada. Portanto, quem consome a biblioteca precisa trocar order.IsPaid() por order.IsPaid, o que configura uma mudança de API.
Por outro lado, manter os métodos como métodos dentro do bloco não exige nenhuma alteração do lado do consumidor. Assim, você decide quando vale a pena expor uma API mais idiomática.
Compatibilidade com extension methods
Uma pergunta natural é o que acontece com os milhares de extension methods existentes. A resposta é tranquilizadora: as duas sintaxes geram a mesma IL.
Em outras palavras, quem consome a biblioteca não percebe diferença entre um método declarado com this e outro declarado em um bloco extension. Além disso, a migração para a sintaxe nova não quebra compatibilidade binária nem de código-fonte.
// Sintaxe clássica
public static class IntExtensions
{
public static bool IsEven(this int number) => number % 2 == 0;
}
// Sintaxe nova, equivalente (em outro arquivo ou projeto)
public static class IntBlockExtensions
{
extension(int number)
{
public bool IsEven() => number % 2 == 0;
}
}
Portanto, você pode converter as suas extensões para extension members aos poucos ou manter a sintaxe antiga onde ela já atende. Os dois estilos convivem no mesmo projeto.
O receiver também aceita ref quando o tipo estendido é um tipo de valor. Nesse caso, um bloco extension(ref int number) altera a variável original, e não uma cópia.
Casos de uso de extension members
O primeiro caso de uso clássico envolve coleções. Em vez de criar classes de coleção específicas, você estende IEnumerable<T> e o comportamento passa a valer para listas, arrays e qualquer outra sequência.
O segundo caso aparece em arquiteturas em camadas, como a Onion Architecture. Nesse cenário, entidades e DTOs transitam entre as camadas com pouco ou nenhum comportamento.
Assim, cada camada declara os extension members de que precisa, como formatações para a interface ou conversões para a persistência. Dessa forma, o domínio não carrega responsabilidades que não são dele.
O terceiro caso envolve tipos do próprio .NET, como string, Stream, Exception ou HttpClient. Por exemplo, uma propriedade IsBlank em string ou um método estático hipotético TimeSpan.FromBusinessDays deixam o código mais próximo da linguagem do negócio.
Por fim, autores de bibliotecas ganham uma forma de evoluir APIs públicas sem tocar nos tipos centrais. Com isso, pacotes complementares podem adicionar propriedades e membros estáticos a tipos de outros pacotes.
Para tipos de valor, existe ainda um recurso útil. Um bloco declarado como extension(ref Account account) recebe a struct por referência, então um método como Deposit altera o saldo da variável original.
Sem o ref, a extensão trabalharia sobre uma cópia, e a alteração se perderia ao fim da chamada. No entanto, mesmo com ref, a extensão continua sem acesso aos membros privados da struct.
Regras de resolução e cuidados
Os extension members não sobrescrevem membros do tipo. Se o tipo já tem um membro com a mesma assinatura, o compilador sempre escolhe o membro original.
Ou seja, uma extensão nunca muda o comportamento de algo que já existe. Ela apenas preenche lacunas, e isso torna o recurso seguro para uso em larga escala.
Também é importante saber que um bloco extension não cria um escopo próprio para nomes. Todos os membros declarados na mesma classe, mesmo em blocos diferentes, precisam ter assinaturas únicas.
Além disso, as extensões respeitam o encapsulamento. Portanto, um extension member não acessa membros privados do tipo estendido.
Por fim, a recomendação oficial continua valendo: se você controla o código do tipo, prefira adicionar o membro nele. Use extension members quando o tipo não é seu, quando herdar não faz sentido ou quando o comportamento é específico de uma camada.
Como começar a usar extension members
O C# 14 acompanha o .NET 10. Por isso, basta instalar o SDK do .NET 10 ou o Visual Studio 2026 e usar net10.0 como target framework.
Com isso, o recurso fica disponível sem nenhuma configuração extra. Em seguida, experimente converter uma classe de extensões existente e compare a legibilidade das duas versões.
Uma boa estratégia é começar pelas extensões internas do projeto, que não afetam consumidores externos. Depois, com a equipe já familiarizada, avalie quais métodos fariam mais sentido como propriedades ou membros estáticos.
Vale também revisar as convenções do time. Por exemplo, defina se novas extensões devem usar sempre blocos extension ou se a sintaxe com this continua aceita em código novo.
O C# 14 também trouxe outras novidades, como a atribuição condicional nula. O artigo sobre atribuição condicional nula no C# 14 mostra esse recurso em detalhes.
Conclusão
Com os extension members, o C# 14 permite escrever código ainda mais limpo, reutilizável e coeso. Propriedades, membros estáticos e operadores agora se juntam aos métodos de extensão.
Essa funcionalidade abre portas para bibliotecas e frameworks adicionarem comportamentos a tipos existentes de forma não intrusiva. Ao mesmo tempo, ela preserva a compatibilidade com todo o código escrito até aqui.
Quer se aprofundar? A documentação oficial do C# 14 traz artigos detalhados sobre a palavra-chave extension e a especificação completa do recurso.
FAQ
Em qual versão do C# surgiram os extension members?
Os extension members surgiram no C# 14, que acompanha o .NET 10. Antes disso, só existiam os extension methods com o modificador this.
Preciso reescrever meus extension methods antigos?
Não, a sintaxe com this continua válida e gera a mesma IL. Você pode migrar aos poucos, sem quebrar quem consome a biblioteca.
É possível criar indexadores de extensão no C# 14?
Não, os indexadores de extensão chegam apenas no C# 15. No C# 14, os blocos aceitam métodos, propriedades e operadores.
Uma propriedade de extensão pode guardar estado?
Não, porque a extensão não adiciona campos ao tipo estendido. A propriedade precisa calcular o valor a partir do receiver ou de outra fonte externa.
Um extension member pode substituir um método existente do tipo?
Não, o compilador sempre prioriza os membros declarados no próprio tipo. A extensão só entra em ação quando não existe membro com a mesma assinatura.