← maurobernal.com.ar

Extension Members en C# 14/15: propiedades y miembros estáticos de extensión como nunca antes

Los métodos de extensión de C# siempre fueron una de las features que más me gustaron. Pero siempre tuvieron una limitación molesta: solo podías agregar métodos, no propiedades. Si querías que lista.IsEmpty funcionara como propiedad (sin paréntesis), no había forma elegante. C# 14 lo soluciona con los bloques de extensión.

El límite de los métodos de extensión clásicos

// ❌ Métodos de extensión clásicos: solo métodos, sintaxis dispersa
public static class EnumerableExtensions
{
    // No hay forma de hacer esto una propiedad — requiere paréntesis
    public static bool IsEmpty<T>(this IEnumerable<T> source) 
        => !source.Any();

    public static IEnumerable<T> WhereNot<T>(
        this IEnumerable<T> source, 
        Func<T, bool> predicate) 
        => source.Where(x => !predicate(x));
}

// Uso: requiere paréntesis aunque semánticamente sea una propiedad
var lista = new List<int>();
bool vacia = lista.IsEmpty();  // paréntesis obligatorios

Extension members: la nueva sintaxis de bloques

// ✅ C# 14: bloques de extensión con propiedades reales
public extension EnumerableExtensions<T>(IEnumerable<T> source)
{
    // Propiedad de extensión: acceso sin paréntesis
    public bool IsEmpty => !source.Any();
    
    public int Count => source.Count();

    // Método de extensión en el mismo bloque
    public IEnumerable<T> WhereNot(Func<T, bool> predicate) 
        => source.Where(x => !predicate(x));

    public IEnumerable<T> Shuffle()
    {
        var lista = source.ToList();
        var rng = Random.Shared;
        for (int i = lista.Count - 1; i > 0; i--)
        {
            int j = rng.Next(i + 1);
            (lista[i], lista[j]) = (lista[j], lista[i]);
        }
        return lista;
    }
}

// Uso: propiedades sin paréntesis
var numeros = new List<int>();
bool vacia = numeros.IsEmpty;   // sin paréntesis
int cant   = numeros.Count;     // sin paréntesis

var pares = Enumerable.Range(1, 10).WhereNot(n => n % 2 == 0);

Miembros estáticos de extensión

Una de las capacidades nuevas más potentes: agregar métodos de fábrica estáticos a tipos que no controlamos.

// Extender DateTime con métodos de fábrica estáticos
public extension DateTimeExtensions(DateTime _)
{
    // Miembro estático: se llama como DateTime.HoyArgentina()
    public static DateTime HoyArgentina()
    {
        var tz = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time");
        return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
    }

    // Propiedad de instancia: sin paréntesis
    public bool EsFinDeSemana => DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;

    public string FormatoArgentino() => ToString("dd/MM/yyyy HH:mm");
}

// Uso
var hoy = DateTime.HoyArgentina();      // método estático
bool finde = DateTime.Now.EsFinDeSemana; // propiedad de instancia
string fmt = DateTime.Now.FormatoArgentino(); // método de instancia

Extensiones para tipos del framework que usamos todos los días

// Extensiones útiles para string
public extension StringExtensions(string source)
{
    public bool IsNullOrEmpty => string.IsNullOrEmpty(source);
    public bool IsNullOrWhiteSpace => string.IsNullOrWhiteSpace(source);
    public string Truncar(int maxLength) 
        => source.Length <= maxLength ? source : source[..maxLength] + "...";
    public string ToCamelCase()
    {
        if (source.IsNullOrEmpty) return source;
        return char.ToLowerInvariant(source[0]) + source[1..];
    }
}

// Extensiones para HttpClient
public extension HttpClientExtensions(HttpClient client)
{
    public async Task<T?> GetJsonAsync<T>(string url)
    {
        var response = await client.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<T>();
    }
}

// Uso
string nombre = "MiServicio";
Console.WriteLine(nombre.ToCamelCase());  // "miServicio"
Console.WriteLine(nombre.Truncar(5));     // "MiSer..."

var usuario = await httpClient.GetJsonAsync<Usuario>("/api/usuarios/1");

Diferencias clave con los métodos de extensión clásicos

CaracterísticaMétodos de extensión (clásico)Extension members (C# 14)
Métodos de instancia
Propiedades de instancia
Miembros estáticos
Agrupación en bloque❌ (clase estática separada)
Compatibilidad hacia atrás✅ C# 3+C# 14+ únicamente

← La palabra clave field en C# 14: adiós para siempre a los backing fields repetitivos | Serie .NET 8 → .NET 10 | Próximo: LINQ en .NET 9 y .NET 10: CountBy, AggregateBy, Index() y las mejoras que cambian cómo consultás datos →

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.