Extension Methods no C#

Extension methods, ou métodos de extensão, são a forma que o C# oferece para adicionar funcionalidades extras a qualquer tipo, sem alterar o código original dele. Neste artigo, você vai entender como eles funcionam, como criar os seus e como aplicá-los em um cenário real do balta.

Vamos partir de um exemplo simples, com a conversão de uma string para DateTime. Em seguida, avançamos para a conversão de entidades em ViewModels, um uso muito comum em aplicações ASP.NET.

O que são extension methods

Segundo a documentação oficial do C#, os extension methods permitem "adicionar" métodos a tipos existentes sem criar um tipo derivado, sem recompilar e sem modificar o tipo original.

Na prática, eles são métodos estáticos que vivem em classes estáticas fora das suas classes. Mesmo assim, você os chama como se fossem métodos de instância do tipo estendido.

Isso permite, entre outras coisas, uma organização melhor do código. Afinal, o comportamento adicional fica separado da classe base e pode morar no assembly ou na camada que fizer mais sentido.

Você provavelmente já usa extension methods todos os dias sem perceber. Por exemplo, os operadores do LINQ, como Where, Select e OrderBy, são métodos de extensão sobre IEnumerable<T>.

Por que usar extension methods

No artigo sobre como gerar arquivos CSV no C#, passamos por uma situação que precisou de um método de extensão para converter uma string em um DateTime. Na verdade, enfrentamos cenários assim quase o tempo todo.

Os implicit operators nos permitem adicionar conversões customizadas dentro de um objeto. No entanto, os extension methods vão além, porque funcionam até em tipos cujo código você não controla.

Pense na conversão de uma string em um formato específico para um DateTime. Embora existam o cast e os métodos de parse, não seria mais fácil ter um método ToDateTime em qualquer string do sistema?

Esse é um cenário em que os extension methods ajudam muito. Não podemos modificar o código interno do .NET, mas podemos estendê-lo com um método adicional na string.

Como criar extension methods no C#

Para criar um método de extensão, você precisa de três elementos. Primeiro, uma classe static; depois, um método também static; por fim, a palavra-chave this antes do primeiro parâmetro.

public static class StringExtensions
{
    public static DateTime ToDateTime(this string value)
    {
        // Espera o formato ano-mês-dia, por exemplo 2022-01-10
        var parts = value.Split("-");
        return new DateTime(
            int.Parse(parts[0]),
            int.Parse(parts[1]),
            int.Parse(parts[2]));
    }
}

O modificador this indica ao compilador qual tipo o método estende. Nesse caso, o primeiro parâmetro é uma string, então todo valor desse tipo passa a enxergar o ToDateTime.

IMPORTANTE: se você está com dúvidas sobre como funcionam as classes estáticas no .NET, dê uma revisada no nosso curso de C# e orientação a objetos da Carreira Backend .NET.

Pronto, isso é tudo que precisamos para usar o método ToDateTime em qualquer string do sistema.

var dateText = "2022-01-10";
var date = dateText.ToDateTime(); // Converte a string para um DateTime

Esse código existe apenas para fins de demonstração. Ou seja, ele não tem tratamento de erro nem otimização.

Uma versão mais segura do ToDateTime

Em produção, uma string fora do formato esperado faria o int.Parse lançar exceção. Por isso, vale usar DateTime.TryParseExact, que informa o sucesso da conversão sem lançar erros.

using System.Globalization;

public static class StringExtensions
{
    public static DateTime? ToDateTime(this string value, string format = "yyyy-MM-dd")
    {
        // Retorna null quando o texto não segue o formato informado
        return DateTime.TryParseExact(
            value,
            format,
            CultureInfo.InvariantCulture,
            DateTimeStyles.None,
            out var result)
            ? result
            : null;
    }
}

Agora o método retorna DateTime? e aceita o formato como parâmetro opcional. Assim, quem chama decide o que fazer quando a conversão falha.

Se quiser se aprofundar nas diferenças entre as formas de conversão, veja o artigo sobre Parse e TryParse no C#. Também vale conferir o guia de DateTime no C# e .NET.

Como o compilador resolve extension methods

Os extension methods são apenas açúcar sintático. Quando você escreve dateText.ToDateTime(), o compilador gera uma chamada equivalente a StringExtensions.ToDateTime(dateText).

Por isso, você também pode chamar o método de forma estática, como qualquer outro membro static. O resultado em IL é exatamente o mesmo.

Existe, porém, uma regra de prioridade importante. Se o tipo já tiver um método de instância com o mesmo nome e assinatura, o compilador sempre escolhe o método do tipo, e a extensão nunca é chamada.

Em outras palavras, extension methods servem para estender, e não para sobrescrever comportamento. Eles também não violam o encapsulamento, porque não acessam membros privados do tipo estendido.

Outro detalhe é o escopo. Os métodos de extensão só ficam disponíveis quando você importa o namespace da classe estática com uma diretiva using.

Extension methods genéricos e em interfaces

Os métodos de extensão não se limitam a classes concretas. Você também pode estender interfaces, e todo tipo que implementar a interface passa a enxergar o método.

Esse é exatamente o truque do LINQ. Como os operadores estendem IEnumerable<T>, eles funcionam em um array, em uma List<T> ou em qualquer coleção que implemente essa interface.

Além disso, os extension methods aceitam parâmetros genéricos e restrições. Veja um exemplo que retorna apenas os itens ativos de qualquer coleção de entidades.

public interface IActivatable
{
    bool Active { get; }
}

public static class ActivatableExtensions
{
    // Funciona para qualquer coleção de tipos que implementem IActivatable
    public static IEnumerable<T> OnlyActive<T>(this IEnumerable<T> items)
        where T : IActivatable
        => items.Where(x => x.Active);
}

Com isso, se Course implementar IActivatable, a chamada courses.OnlyActive() passa a funcionar. Da mesma forma, qualquer outra entidade com a propriedade Active ganha o mesmo comportamento sem repetir código.

Antigamente, era comum criar "classes de coleção" específicas para cada tipo, com métodos que operavam sobre a lista. A documentação lembra que a mesma funcionalidade pode ser alcançada com uma extensão sobre IEnumerable<T>, com a vantagem de servir para qualquer coleção.

Extension methods com ref em structs

Estender structs exige um cuidado extra, porque elas são passadas por valor. Ou seja, qualquer alteração feita dentro do método de extensão acontece em uma cópia e se perde quando o método termina.

Para resolver isso, você pode adicionar o modificador ref ao primeiro parâmetro. Assim, o método recebe a struct por referência e consegue alterar o valor original.

public static class IntExtensions
{
    // Altera apenas a cópia, o valor original continua o mesmo
    public static void Increment(this int number)
        => number++;

    // Recebe por referência, então altera o valor original
    public static void RefIncrement(this ref int number)
        => number++;
}

Nesse exemplo, chamar x.Increment() não muda o valor de x. Por outro lado, x.RefIncrement() incrementa a variável de verdade.

Estendendo objetos com extension methods

No artigo de implicit operators, mostrei a conversão de um tipo de objeto para outro. Esse recurso amplia bastante o seu leque e pode até dispensar bibliotecas como o AutoMapper em determinados cenários.

Porém, há casos em que não sabemos para qual ou quais objetos vamos converter. Pior ainda, às vezes os objetos de destino ficam em outro assembly.

No próprio balta, temos uma biblioteca com todas as entidades, que refletem os modelos do banco de dados. Ela fica em um pacote chamado Balta.Core, distribuído via GitHub Packages.

Nessa biblioteca, por exemplo, temos uma entidade chamada Course, que representa um curso, com as propriedades a seguir.

public class Course : Entity
{
    public string Title { get; set; }
    public string Summary { get; set; }
    public string Url { get; set; }
    public EContentLevel Level { get; set; }
    public int DurationInMinutes { get; set; }
    public DateTime CreateDate { get; set; }
    public DateTime LastUpdateDate { get; set; } = DateTime.UtcNow;
    public bool Active { get; set; } = true;
    public bool Free { get; set; } = false;
    public bool Featured { get; set; } = false;
    public Guid AuthorId { get; set; }
    public Author Author { get; set; }
    public Guid CategoryId { get; set; }
    public Category Category { get; set; }
    public string Tags { get; set; }
}

Esse formato faz todo sentido para o modelo e para o reflexo do objeto no banco de dados. Contudo, ele não faz sentido algum para as telas, porque o usuário enxerga uma porcentagem bem menor dessas informações.

ViewModels e DTOs

Para resolver isso, usamos ViewModels ou DTOs, que são modelos especiais para transportar dados de um lugar para outro. Neste caso, eles levam os dados da camada de baixo até a camada de apresentação, o ASP.NET.

public class CourseCardViewModel
{
    public string Title { get; set; }
    public string Url { get; set; }
    public EContentLevel Level { get; set; }
    public int DurationInMinutes { get; set; }
}

Os extension methods se encaixam exatamente nesta parte. Temos uma lista de cursos (List<Course>) que vem do cache e precisamos convertê-la para uma List<CourseCardViewModel>.

A abordagem pelo construtor

Eu já usei bastante a abordagem do construtor, criando uma sobrecarga que recebia um Course e fazia a conversão ali mesmo. Funciona bem, mas o código pode ficar extenso à medida que surgem conversões de diversos objetos.

public class CourseCardViewModel
{
    public CourseCardViewModel(Course course)
    {
        // Preenche as propriedades
        Title = course.Title;
        Url = course.Url;
        Level = course.Level;
        DurationInMinutes = course.DurationInMinutes;
    }

    public string Title { get; set; }
    public string Url { get; set; }
    public EContentLevel Level { get; set; }
    public int DurationInMinutes { get; set; }
}

Com esse construtor, a conversão de uma lista de Course em uma lista de CourseCardViewModel fica assim.

var cards = courses.Select(x => new CourseCardViewModel(x));

O problema dessa abordagem é o acoplamento. A ViewModel passa a conhecer a entidade, e cada nova origem de dados exige mais um construtor.

Aplicando extension methods na conversão

Neste caso, optei por uma pasta chamada Extensions, onde organizo tudo por features, assim como faço no resto do projeto. Dessa forma, os métodos de extensão ficam todos ali dentro.

public static class CourseExtensions
{
    public static CourseCardViewModel ToCard(this Course course)
    {
        // Gera um novo ViewModel a partir da entidade e retorna
        return new CourseCardViewModel
        {
            Title = course.Title,
            Url = course.Url,
            Level = course.Level,
            DurationInMinutes = course.DurationInMinutes
        };
    }
}

Repare no this antes do parâmetro Course. Sem ele, ToCard seria apenas um método estático comum, e a chamada course.ToCard() não compilaria.

Agora temos liberdade para criar quantas extensões quisermos para o Course, sem modificar o código dele nem o da ViewModel. Além disso, o uso fica bem simples.

Convertendo a lista de cursos

O exemplo abaixo converte uma lista de Course em uma lista de CourseCardViewModel.

var cards = courses.Select(x => x.ToCard());

Também podemos ir um passo além e estender a própria coleção. Assim, a conversão da lista inteira vira uma única chamada.

public static class CourseExtensions
{
    public static IEnumerable<CourseCardViewModel> ToCards(this IEnumerable<Course> courses)
        => courses.Select(x => x.ToCard());
}

Com isso, o código da controller ou da página fica apenas com courses.ToCards(). Esse é o mesmo princípio que o LINQ usa para adicionar operadores a qualquer coleção.

Boas práticas com extension methods

A documentação recomenda preferir a alteração do próprio tipo ou a herança sempre que isso for razoável e possível. Os extension methods brilham quando o código original não está sob seu controle, quando herdar não faz sentido ou quando a funcionalidade tem escopo limitado.

Organize as extensões em namespaces bem definidos. Como o using importa todas as classes estáticas de um namespace de uma vez, misturar extensões de assuntos diferentes polui o IntelliSense.

Também evite nomes genéricos demais em tipos muito usados, como string e object. Afinal, um ToDateTime em toda string do sistema é útil, mas dezenas de extensões nesse tipo confundem quem lê o código.

Outra prática é manter as extensões sem efeitos colaterais inesperados. Um método ToCard, por exemplo, deve apenas converter, sem acessar banco de dados ou serviços externos.

Por fim, lembre que um método de extensão é um método estático. Por isso, ele é fácil de testar isoladamente, já que basta chamar o método com uma entrada e verificar a saída.

Testando extension methods

Como são métodos estáticos e, em geral, sem estado, os métodos de extensão estão entre os trechos de código mais fáceis de testar. Basta montar a entrada, chamar o método e comparar a saída.

public class CourseExtensionsTests
{
    [Fact]
    public void ToCard_ShouldCopyTitleAndUrl()
    {
        var course = new Course { Title = "C# fundamentals", Url = "csharp-fundamentals" };

        var card = course.ToCard();

        Assert.Equal("C# fundamentals", card.Title);
        Assert.Equal("csharp-fundamentals", card.Url);
    }
}

O mesmo vale para o ToDateTime. Então, vale cobrir o formato válido e também textos inválidos, para garantir que o método retorna null em vez de lançar exceção.

Se você está começando com testes, o artigo sobre fundamentos dos testes de unidade é um bom ponto de partida.

Cuidados ao usar extension methods

Um detalhe que surpreende muita gente é o comportamento com valores nulos. Como a chamada vira um método estático, value.ToDateTime() não lança NullReferenceException no ponto da chamada quando value é null.

Nesse caso, o método recebe null no parâmetro marcado com this. Por isso, vale validar a entrada logo no início, como faz o código abaixo.

public static DateTime? ToDateTime(this string? value, string format = "yyyy-MM-dd")
{
    // Evita erro quando a extensão é chamada sobre uma referência nula
    if (string.IsNullOrWhiteSpace(value))
        return null;

    return DateTime.TryParseExact(value, format, CultureInfo.InvariantCulture,
        DateTimeStyles.None, out var result) ? result : null;
}

Outro cuidado envolve bibliotecas que você mesmo mantém. A documentação recomenda não usar extensões apenas para evitar incrementar a versão de um assembly; se a funcionalidade é importante, ela deve entrar no próprio tipo, seguindo as regras de versionamento.

Também evite esconder regras de negócio em extensões espalhadas pelo projeto. Quando um comportamento é parte essencial da entidade, ele pertence à entidade, e não a uma classe estática em outra pasta.

Extension methods e o C# 14

Os exemplos deste artigo usam a sintaxe clássica, com this no primeiro parâmetro, que funciona desde o C# 3. A partir do C# 14, a linguagem também oferece os blocos extension, que permitem declarar propriedades, operadores e membros estáticos de extensão.

A sintaxe clássica continua válida, e as duas formas geram o mesmo IL. Se quiser conhecer a novidade, veja o artigo sobre o novo modelo de extension members do C# 14.

Conclusão

Os extension methods permitem adicionar funcionalidades a qualquer tipo no C#, seja ele nativo ou complexo. Além disso, fazem isso de forma simples e organizada, separando o comportamento adicional da classe base.

No balta, eles resolveram a conversão de entidades em ViewModels sem acoplar as duas pontas. Portanto, sempre que você se pegar repetindo uma conversão ou um utilitário em vários lugares, considere criar um método de extensão.

Comece pelos casos simples, como conversões e utilitários de texto, e evolua para extensões sobre interfaces e coleções conforme o projeto crescer.

FAQ

Extension methods alteram a classe original?

Não. O método fica em uma classe estática separada, e o compilador apenas permite chamá-lo com a sintaxe de método de instância.

Um extension method pode acessar membros privados?

Não, porque ele respeita o encapsulamento do tipo estendido. Somente os membros acessíveis de fora ficam disponíveis.

O que acontece se o tipo já tiver um método com a mesma assinatura?

O compilador sempre escolhe o método do próprio tipo. Por isso, a extensão com a mesma assinatura nunca é chamada.

Por que meu extension method não aparece no IntelliSense?

Normalmente falta a diretiva using do namespace onde está a classe estática. Também vale conferir se o primeiro parâmetro tem o modificador this.

Extension methods substituem o AutoMapper?

Em cenários simples, sim, pois a conversão fica explícita e fácil de depurar. Em mapeamentos muito numerosos, cada time avalia o custo de escrever as conversões manualmente.

Glossário

Termo Significado
Extension method Método estático chamado como se fosse um método de instância de outro tipo
Classe estática Classe que não pode ser instanciada e contém apenas membros estáticos
this (modificador) Palavra-chave no primeiro parâmetro que indica o tipo estendido
ViewModel Modelo com os dados que uma tela precisa exibir
DTO Objeto de transferência de dados entre camadas ou sistemas
LINQ Conjunto de operadores de consulta implementados como métodos de extensão
IL Linguagem intermediária gerada pelo compilador do C#
Implicit operator Operador que define uma conversão implícita entre dois tipos

Fontes