Extension methods и fluent API

↑ 2.1 C♯: продвинутый язык · ← Предыдущая · → Следующая

Обязательно~6 мин чтенияУровень: middle

Зачем это на собесе

extension-методы — основа LINQ и всей конфигурации ASP.NET Core (services.AddXxx(), app.UseXxx()). В проекте общие пакеты (core/extensions, http_shared/DI) подключаются именно так.

Объяснение

Extension-метод — статический метод в статическом классе, первый параметр помечен this. Вызывается как метод экземпляра.

public static class StringExtensions
{
    public static string? NullIfEmpty(this string? s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
    public static string Truncate(this string s, int max) => s.Length <= max ? s : s[..max] + "…";
}
 
var middle = dto.MiddleName.NullIfEmpty();   // компилятор превращает в StringExtensions.NullIfEmpty(dto.MiddleName)
  • Не имеют доступа к private-членам типа, только к публичному API.
  • Не переопределяют методы: если у типа есть метод с той же сигнатурой, вызовется метод экземпляра.
  • Разрешаются статически по типу переменной и подключённым using.
  • Можно вызвать на null (это обычный статический вызов), поэтому проверяйте аргумент.

Расширения для DI и pipeline (главный практический кейс)

// пакет core/http_shared — одна строка подключения во всех сервисах
public static class HttpSharedExtensions
{
    public static IServiceCollection AddHttpShared(this IServiceCollection services, IConfiguration cfg)
    {
        services.AddHttpContextAccessor();
        services.AddScoped<IUserContext, UserContext>();
        services.AddExceptionHandler<GlobalExceptionHandler>();
        services.AddProblemDetails();
        services.Configure<KeycloakOptions>(cfg.GetSection("Keycloak"));
        return services;                              // возвращаем для цепочки (fluent)
    }
 
    public static IApplicationBuilder UseHttpShared(this IApplicationBuilder app) =>
        app.UseExceptionHandler().UseMiddleware<CorrelationIdMiddleware>();
}
 
// Program.cs сервиса
builder.Services.AddHttpShared(builder.Configuration).AddTracing(builder.Configuration);
app.UseHttpShared();

Расширения для интерфейсов и generic

public static class QueryableExtensions
{
    public static IQueryable<T> Page<T>(this IQueryable<T> q, int page, int size) =>
        q.Skip((page - 1) * size).Take(size);
 
    public static IQueryable<T> WhereIf<T>(this IQueryable<T> q, bool condition, Expression<Func<T, bool>> pred) =>
        condition ? q.Where(pred) : q;
}
 
var result = await db.Patients
    .WhereIf(filter.City is not null, p => p.City == filter.City)
    .OrderBy(p => p.LastName)
    .Page(filter.Page, 50)
    .ToListAsync(ct);

Fluent API

Цепочка вызовов, где каждый метод возвращает объект для продолжения: builder.Services.AddX().AddY(), EF Core modelBuilder.Entity<T>().HasKey(...).HasIndex(...), FluentValidation RuleFor(x => x.Name).NotEmpty().MaximumLength(100), построители отчётов.

C# 14: extension members

Новый синтаксис блоков extension(Type t) { ... } — можно объявлять extension-свойства и статические члены, а не только методы.

Нюансы и подводные камни

  • Экстеншены на object или string засоряют IntelliSense по всему проекту. Держите их в узких namespace.
  • Бизнес-логика в extension-методах плохо тестируется и прячется от DI. Место экстеншенов — утилиты и конфигурация.
  • Конфликт имён двух экстеншенов из разных using → ошибка неоднозначности.
  • Extension-метод над IEnumerable<T> и IQueryable<T> с одинаковым именем: выбор зависит от статического типа. Легко случайно уйти в память.

Тестирование

[Theory]
[InlineData("  ", null)]
[InlineData(" Иван ", "Иван")]
public void NullIfEmpty_normalizes(string input, string? expected) =>
    input.NullIfEmpty().Should().Be(expected);

Вопросы с ответами

Связанные темы