Introdução ao Fody para Transformação de Assemblies .NET

Visão Geral

O Fody representa uma ferramenta extensível dedicada à manipulação de assemblies .NET através da injeção de código IL durante o processo de compilação. Esta técnica permite adicionar funcionalidades automáticas aos programas sem necessidade de escrever código repetitivo manualmente.

Este artigo apresenta dois plugins amplamente utilizados: PropertyChanged.Fody para implementação automática de bindings em ViewModels, e MethodBoundaryAspect.Fody para programação orientada a aspectos (AOP).

PropertyChanged.Fody

O plugin PropertyChanged.Fody automatiza a implementação da interface INotifyPropertyChanged, eliminando a necessidade de escrever manualmente o código de notificação para cada propriedade.

Considere o seguinte ViewModel básico:

public class ClienteViewModel : INotifyPropertyChanged
{
    public string Nome { get; set; } = "João Silva";

    public event PropertyChangedEventHandler? PropertyChanged;
}

Após a transformação pelo Fody, o código injeta automaticamente as chamadas ao método OnPropertyChanged dentro dos setters das propriedades:

public class ClienteViewModel : INotifyPropertyChanged
{
    private string _nome = "João Silva";
    public string Nome
    {
        get => _nome;
        set
        {
            _nome = value;
            OnPropertyChanged(nameof(Nome));
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Personalizando Comportamentos

Para executar lógica customizada quando uma propriedade sofre alteração, basta definir um método nomeado no formato On[Propriedade]Changed():

public class ProdutoViewModel : INotifyPropertyChanged
{
    private decimal _preco;
    public decimal Preco
    {
        get => _preco;
        set
        {
            _preco = value;
            OnPropertyChanged(nameof(Preco));
        }
    }

    private void OnPrecoChanged()
    {
        Console.WriteLine("O preço do produto foi modificado!");
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

Para excluir determinada propriedade da injeção automática, utilize o atributo DoNotNotify:

public class PedidoViewModel : INotifyPropertyChanged
{
    [DoNotNotify]
    public int Identificador { get; set; }

    public string Descricao { get; set; } = string.Empty;

    public event PropertyChangedEventHandler? PropertyChanged;
}

O atributo AddINotifyPropertyChangedInterface aplicado à classe dispensa a implementação manual da interface:

[AddINotifyPropertyChangedInterface]
public class UsuarioViewModel
{
    public string Email { get; set; } = string.Empty;
    public string Telefone { get; set; } = string.Empty;
}

O PropertyChanged.Fody também suporta propagação automática de notificações entre propriedades relacionadas através do atributo AlsoNotifyFor.

MethodBoundaryAspect.Fody

Este plugin facilita a implementação de programação orientada a aspectos, permitiendo inserir código personalizado nos pontos de entrada, saída e exceção de métodos.

Imagine um cenário onde é necesário gerenciar o ciclo de vida de uma conexão de banco de dados: abrir, configurar, executar queries, fechar e liberar recursos. Cada operação precisa de tratamento de erros consistente:

internal class ServicoBancoDados
{
    public void Conectar()
    {
        try
        {
            // Lógica de conexão com banco
        }
        catch (Exception ex)
        {
            throw new Exception($"Falha na conexão: {ex.Message}");
        }
    }

    public void Configurar()
    {
        try
        {
            // Configurações do banco
        }
        catch (Exception ex)
        {
            throw new Exception($"Falha na configuração: {ex.Message}");
        }
    }

    public void ExecutarQuery(string consulta)
    {
        try
        {
            // Execução da query
        }
        catch (Exception ex)
        {
            throw new Exception($"Falha na execução: {ex.Message}");
        }
    }

    public void Fechar()
    {
        try
        {
            // Fechar conexão
        }
        catch (Exception ex)
        {
            throw new Exception($"Falha ao fechar: {ex.Message}");
        }
    }

    public void Dispose()
    {
        try
        {
            // Liberar recursos
        }
        catch (Exception ex)
        {
            throw new Exception($"Falha no dispose: {ex.Message}");
        }
    }
}

O código cliente também precisa de tratamento estruturado:

try
{
    var servico = new ServicoBancoDados();
    servico.Conectar();
    servico.Configurar();
    servico.ExecutarQuery("SELECT * FROM Usuarios");
    servico.Fechar();
    servico.Dispose();
    
    MessageBox.Show("Operação concluída");
}
catch (Exception ex)
{
    MessageBox.Show(ex.Message);
}

Smiplificando com AOP

Utilizando o MethodBoundaryAspect.Fody, o tratamento de erros e logging podem ser centralizados em um aspecto reutilizável:

internal class AspectoTryCatch : OnMethodBoundaryAspect
{
    public override void OnException(MethodExecutionArgs args)
    {
        throw new Exception($"Erro em {args.Method.Name}: {args.Exception.Message}");
    }

    public override void OnEntry(MethodExecutionArgs args)
    {
        Console.WriteLine($"Entrando no método: {args.Method.Name}");
        base.OnEntry(args);
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        Console.WriteLine($"Saindo do método: {args.Method.Name}");
        base.OnExit(args);
    }
}

A classe de serviço com o aspecto aplicado fica extremamente limpa:

[AspectoTryCatch]
internal class ServicoBancoDadosAOP
{
    public void Conectar() { }

    public void Configurar() { }

    public void ExecutarQuery(string consulta) { }

    public void Fechar() { }

    public void Dispose() { }
}

A saída no console demonstra a execução automática do aspecto:

Entrando no método: Conectar
Saindo do método: Conectar
Entrando no método: Configurar
Saindo do método: Configurar
Entrando no método: ExecutarQuery
Saindo do método: ExecutarQuery
Entrando no método: Fechar
Saindo do método: Fechar
Entrando no método: Dispose
Saindo no método: Dispose

Esta abordagem reduz significativamente a duplicação de código e mantém a lógica de cross-cutting concerns isolada em um único local.

Publicado em 7-26 09:14