EF Core 명명된 쿼리 필터
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 스키마
| 컬럼 | 타입 | 설명 |
|---|---|---|
| Id | int | 기본 키 |
| FirstName | string | 필수, 최대 길이 50 |
| LastName | string | 필수, 최대 길이 50 |
| IsDeleted | bool | 소프트‑삭제 플래그 |
| IsManager | bool | 직원이 매니저인지 여부를 나타냄 |
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을 사용하면 여러 명명된 쿼리 필터를 정의하고 관리하는 것이 매우 간단해집니다. 위 예제를 따라 하면 개발자는 소프트 삭제, 역할 기반 필터링, 다중 테넌시 등 일반적인 시나리오를 구현하면서 각 쿼리에 적용되는 필터를 세밀하게 제어할 수 있습니다.