EF Core 명명된 쿼리 필터

발행: (2026년 5월 19일 AM 09:20 GMT+9)
4 분 소요
원문: Dev.to

Source: Dev.to

소개

EF Core 10은 명명된 쿼리 필터를 도입합니다. 이는 기존의 전역 쿼리 필터를 한 단계 발전시킨 기능입니다.
엔터티당 하나의 결합된 필터 대신, 이제 필터에 개별 이름(SoftDelete, Tenant 등)을 부여할 수 있습니다. 이를 통해 동일 엔터티에 여러 필터를 적용하고, 특정 LINQ 쿼리에서 필요한 필터만 선택적으로 비활성화할 수 있어 기본 필터링 동작을 깔끔하고 일관되며 중앙 집중식으로 유지할 수 있습니다. [learn.microsoft.com]

필터 생성

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity(entity =>
    {
        entity.Property(e => e.FirstName)
              .IsRequired()
              .HasMaxLength(50);
        entity.Property(e => e.LastName)
              .IsRequired()
              .HasMaxLength(50);

        // 명명된 쿼리 필터
        entity.HasQueryFilter("SoftDelete", e => !e.IsDeleted);
        entity.HasQueryFilter("IsManager",  e => e.IsManager);
    });

    OnModelCreatingPartial(modelBuilder);
}

Employee 스키마

컬럼타입설명
Idint기본 키
FirstNamestring필수, 최대 길이 50
LastNamestring필수, 최대 길이 50
IsDeletedbool소프트‑삭제 플래그
IsManagerbool직원이 매니저인지 여부를 나타냄

Soft Delete를 위한 SaveChanges 오버라이드

public override async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
    ChangeTracker.DetectChanges();

    foreach (var entry in ChangeTracker.Entries())
    {
        if (entry.State == EntityState.Deleted)
        {
            // 삭제를 소프트‑삭제로 변환
            entry.State = EntityState.Modified;
            entry.Property("IsDeleted").CurrentValue = true;
        }
    }

    return await base.SaveChangesAsync(cancellationToken);
}

public override int SaveChanges()
{
    ChangeTracker.DetectChanges();

    foreach (var entry in ChangeTracker.Entries())
    {
        if (entry.State == EntityState.Deleted)
        {
            entry.State = EntityState.Modified;
            entry.Property("IsDeleted").CurrentValue = true;
        }
    }

    return base.SaveChanges();
}

필터 무시

using var context = new Context();
var employees = context.Employees
    .IgnoreQueryFilters(["SoftDelete"])
    .ToList();

직원 삭제 (데모)

private static async Task PerformDelete()
{
    SpectreConsoleHelpers.PrintPink();

    int id = 2;

    await using var context = new Context();
    var employee = await context.Employees.FirstOrDefaultAsync(x => x.Id == id);

    if (employee is not null)
    {
        context.Employees.Remove(employee).State = EntityState.Deleted;
        var affected = await context.SaveChangesAsync();

        AnsiConsole.MarkupLine(affected > 0
            ? $"[green]Successfully deleted employee with ID {id}.[/]"
            : $"[red]Failed to delete employee with ID {id}. Affected rows: {affected}[/]");
    }
    else
    {
        AnsiConsole.MarkupLine($"[yellow]Employee with ID {id} not found.[/]");
    }
}

확장 메서드로 쿼리 필터 확인

public static class DbContextExtensions
{
    public static bool HasQueryFilter(this DbContext context)
        where TEntity : class
    {
        var entityType = context.Model.FindEntityType(typeof(TEntity));
        return entityType?.GetDeclaredQueryFilters() != null;
    }

    public static IReadOnlyCollection? GetQueryFilters(this DbContext context)
        where TEntity : class
    {
        var entityType = context.Model.FindEntityType(typeof(TEntity));
        return entityType?.GetDeclaredQueryFilters();
    }

    public static IReadOnlyCollection TryGetQueryFilters(this DbContext context)
        where TEntity : class
    {
        var entityType = context.Model.FindEntityType(typeof(TEntity));
        var filters = entityType?.GetDeclaredQueryFilters();
        return filters ?? [];
    }
}

Employee에 정의된 필터 표시

private static void DisplayEmployeeQueryFilters()
{
    using var context = new Context();

    if (context.HasQueryFilter())
    {
        var filters = context.GetQueryFilters();
        if (filters is null) return;

        foreach (var (index, filter) in filters.Index())
        {
            AnsiConsole.MarkupLine($"{index,-4}[cyan]Name[/] {filter.Key} [cyan]Expression[/] {filter.Expression}");
        }
    }
    else
    {
        AnsiConsole.MarkupLine("[red]No query filters found for Employee entity.[/]");
    }
}

요약

EF Core 10을 사용하면 여러 명명된 쿼리 필터를 정의하고 관리하는 것이 매우 간단해집니다. 위 예제를 따라 하면 개발자는 소프트 삭제, 역할 기반 필터링, 다중 테넌시 등 일반적인 시나리오를 구현하면서 각 쿼리에 적용되는 필터를 세밀하게 제어할 수 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »

dotnet Framework 수명 주기 도구

Introduction Learn how to create a dotnet Global Tool that lists all .NET Core frameworks with release and end‑of‑life information. 💡 For my other article on...

클린 아키텍처 in .NET 설명 (The Dependency Rule)

EF Core를 업그레이드하면서 300개의 파일을 수정해야 했던 적이 있거나, 단일 비즈니스 규칙을 단위 테스트하려고 했는데 먼저 실행 중인 데이터베이스가 필요하다는 것을 깨달았다면 — 당신은...