# Interceptors Advanced query customization with interceptors. ## Overview `IDynamicQueryInterceptorProvider` enables deep customization of PoweredSoft.DynamicQuery behavior: - ✅ **Custom operators** - Add new filter operators - ✅ **Query transformation** - Modify queries before execution - ✅ **Logging/monitoring** - Track query performance - ✅ **Default behaviors** - Override framework defaults - ✅ **Complex filtering** - Implement advanced filter logic - ✅ **Extensibility** - Extend framework capabilities **Note:** Interceptors are advanced features. Most scenarios are better handled with `IAlterQueryableService` or `IQueryableProvider`. ## IDynamicQueryInterceptorProvider Interface ```csharp public interface IDynamicQueryInterceptorProvider { List GetInterceptors( IQueryable queryable, IDynamicQuery query); } ``` ## Available Interceptors PoweredSoft.DynamicQuery provides several interceptor types: - **IFilterInterceptor** - Customize filter behavior - **ISortInterceptor** - Customize sort behavior - **IGroupInterceptor** - Customize group behavior - **IAggregateInterceptor** - Customize aggregate behavior - **IBeforeQueryPageInterceptor** - Intercept before pagination - **IQueryConvertInterceptor** - Customize result conversion ## Custom Filter Operator ### Implementing Custom Operator ```csharp public class CustomFilterInterceptor : IFilterInterceptor { public bool CanHandle(IFilter filter) { // Handle custom "IsWeekday" operator return filter is Filter f && f.Type == FilterType.Custom && f.CustomOperator == "IsWeekday"; } public Expression> GetExpression(IFilter filter) { var path = filter.Path; return entity => { var value = GetPropertyValue(entity, path) as DateTime?; if (!value.HasValue) return false; return value.Value.DayOfWeek != DayOfWeek.Saturday && value.Value.DayOfWeek != DayOfWeek.Sunday; }; } private object? GetPropertyValue(object obj, string propertyPath) { var properties = propertyPath.Split('.'); object current = obj; foreach (var prop in properties) { var propertyInfo = current.GetType().GetProperty(prop); if (propertyInfo == null) return null; current = propertyInfo.GetValue(current); if (current == null) return null; } return current; } } ``` ### Provider Implementation ```csharp public class CustomInterceptorProvider : IDynamicQueryInterceptorProvider { public List GetInterceptors( IQueryable queryable, IDynamicQuery query) { return new List { new CustomFilterInterceptor() }; } } ``` ### Registration ```csharp builder.Services.AddDynamicQuery() .AddDynamicQueryWithProvider() .AddDynamicQueryInterceptorProvider(); ``` ### Usage ```json { "filters": [ { "path": "orderDate", "operator": "Custom", "customOperator": "IsWeekday" } ] } ``` ## Logging Interceptor ### Query Logging ```csharp public class LoggingInterceptor : IBeforeQueryPageInterceptor { private readonly ILogger _logger; public LoggingInterceptor(ILogger logger) { _logger = logger; } public void InterceptBeforeQueryPage(IQueryable queryable) { var sql = queryable.ToQueryString(); _logger.LogInformation("Executing dynamic query: {SQL}", sql); var stopwatch = Stopwatch.StartNew(); // Query will execute after this interceptor stopwatch.Stop(); _logger.LogInformation("Query completed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); } } public class LoggingInterceptorProvider : IDynamicQueryInterceptorProvider { private readonly ILogger _logger; public LoggingInterceptorProvider(ILogger logger) { _logger = logger; } public List GetInterceptors( IQueryable queryable, IDynamicQuery query) { return new List { new LoggingInterceptor(_logger) }; } } ``` ## Performance Monitoring ### Query Performance Interceptor ```csharp public class PerformanceInterceptor : IBeforeQueryPageInterceptor { private readonly IMetricsCollector _metrics; public PerformanceInterceptor(IMetricsCollector metrics) { _metrics = metrics; } public void InterceptBeforeQueryPage(IQueryable queryable) { var stopwatch = Stopwatch.StartNew(); // Record query execution time var expression = queryable.Expression.ToString(); var complexity = CalculateComplexity(expression); _metrics.RecordQueryExecution(new QueryMetrics { Complexity = complexity, Timestamp = DateTime.UtcNow, QueryExpression = expression }); } private int CalculateComplexity(string expression) { // Simple complexity calculation based on expression length and operations var whereCount = Regex.Matches(expression, "Where").Count; var joinCount = Regex.Matches(expression, "Join").Count; var orderByCount = Regex.Matches(expression, "OrderBy").Count; return whereCount + (joinCount * 2) + orderByCount; } } ``` ## Default Value Interceptor ### Auto-Apply Default Filters ```csharp public class DefaultFilterInterceptor : IFilterInterceptor { public bool CanHandle(IFilter filter) { // This interceptor handles all filters to add defaults return true; } public Expression> GetExpression(IFilter filter) { // If filtering on a date and no time component specified, // default to start of day if (filter.Path.Contains("Date") && filter.Value is DateTime date) { if (date.TimeOfDay == TimeSpan.Zero) { // Adjust filter to match entire day var endOfDay = date.AddDays(1); return entity => { var value = GetPropertyValue(entity, filter.Path) as DateTime?; return value >= date && value < endOfDay; }; } } // Let default handling proceed return null; } } ``` ## Case-Insensitive Filter ### Case-Insensitive String Matching ```csharp public class CaseInsensitiveFilterInterceptor : IFilterInterceptor { public bool CanHandle(IFilter filter) { return filter is Filter f && f.Type == FilterType.String && f.Value is string; } public Expression> GetExpression(IFilter filter) { var f = filter as Filter; var searchValue = (f.Value as string)?.ToLower(); switch (f.Operator) { case FilterOperator.Contains: return entity => { var value = GetPropertyValue(entity, f.Path) as string; return value != null && value.ToLower().Contains(searchValue); }; case FilterOperator.StartsWith: return entity => { var value = GetPropertyValue(entity, f.Path) as string; return value != null && value.ToLower().StartsWith(searchValue); }; case FilterOperator.EndsWith: return entity => { var value = GetPropertyValue(entity, f.Path) as string; return value != null && value.ToLower().EndsWith(searchValue); }; default: return null; // Use default handling } } } ``` ## Query Validation Interceptor ### Validate Query Complexity ```csharp public class QueryValidationInterceptor : IBeforeQueryPageInterceptor { private const int MaxFilterCount = 10; private const int MaxSortCount = 5; public void InterceptBeforeQueryPage(IQueryable queryable) { var expression = queryable.Expression.ToString(); var filterCount = Regex.Matches(expression, "Where").Count; var sortCount = Regex.Matches(expression, "OrderBy").Count; if (filterCount > MaxFilterCount) { throw new InvalidOperationException( $"Query has too many filters ({filterCount}). Maximum is {MaxFilterCount}."); } if (sortCount > MaxSortCount) { throw new InvalidOperationException( $"Query has too many sorts ({sortCount}). Maximum is {MaxSortCount}."); } } } ``` ## Composite Interceptor Provider ### Multiple Interceptors ```csharp public class CompositeInterceptorProvider : IDynamicQueryInterceptorProvider { private readonly ILogger _logger; private readonly IMetricsCollector _metrics; public CompositeInterceptorProvider( ILogger logger, IMetricsCollector metrics) { _logger = logger; _metrics = metrics; } public List GetInterceptors( IQueryable queryable, IDynamicQuery query) { return new List { new LoggingInterceptor(_logger), new PerformanceInterceptor(_metrics), new QueryValidationInterceptor(), new CaseInsensitiveFilterInterceptor(), new CustomFilterInterceptor() }; } } ``` ## Testing Interceptors ### Unit Tests ```csharp public class CustomFilterInterceptorTests { private readonly CustomFilterInterceptor _interceptor; public CustomFilterInterceptorTests() { _interceptor = new CustomFilterInterceptor(); } [Fact] public void CanHandle_WithIsWeekdayOperator_ReturnsTrue() { var filter = new Filter { Path = "orderDate", Type = FilterType.Custom, CustomOperator = "IsWeekday" }; var result = _interceptor.CanHandle(filter); Assert.True(result); } [Theory] [InlineData("2024-01-01")] // Monday [InlineData("2024-01-02")] // Tuesday [InlineData("2024-01-05")] // Friday public void GetExpression_WithWeekday_ReturnsTrue(string dateString) { var filter = new Filter { Path = "orderDate", Type = FilterType.Custom, CustomOperator = "IsWeekday" }; var expression = _interceptor.GetExpression(filter); var compiled = expression.Compile(); var order = new Order { OrderDate = DateTime.Parse(dateString) }; Assert.True(compiled(order)); } [Theory] [InlineData("2024-01-06")] // Saturday [InlineData("2024-01-07")] // Sunday public void GetExpression_WithWeekend_ReturnsFalse(string dateString) { var filter = new Filter { Path = "orderDate", Type = FilterType.Custom, CustomOperator = "IsWeekday" }; var expression = _interceptor.GetExpression(filter); var compiled = expression.Compile(); var order = new Order { OrderDate = DateTime.Parse(dateString) }; Assert.False(compiled(order)); } } ``` ## When to Use Interceptors ### ✅ Use Interceptors For: - Custom filter operators not supported by framework - Query logging and monitoring - Performance tracking - Query validation - Default value injection - Complex filter logic ### ❌ Use Alternatives For: - **Security filtering** → Use `IAlterQueryableService` - **Tenant isolation** → Use `IAlterQueryableService` - **Default base filters** → Use `IQueryableProvider` - **Simple customization** → Use query parameters ## Best Practices ### ✅ DO - Keep interceptors focused and single-purpose - Test interceptors independently - Document custom operators - Use descriptive operator names - Return null to use default handling - Log complex query executions ### ❌ DON'T - Don't use interceptors for security (use IAlterQueryableService) - Don't perform synchronous I/O in interceptors - Don't modify queryable in filter interceptors - Don't throw exceptions without validation - Don't create overly complex interceptors - Don't skip testing custom operators ## Common Patterns ### Pattern: Conditional Interceptor ```csharp public class ConditionalInterceptorProvider : IDynamicQueryInterceptorProvider { private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; public ConditionalInterceptorProvider( IWebHostEnvironment environment, ILogger logger) { _environment = environment; _logger = logger; } public List GetInterceptors( IQueryable queryable, IDynamicQuery query) { var interceptors = new List(); // Only log in development if (_environment.IsDevelopment()) { interceptors.Add(new LoggingInterceptor(_logger)); } // Always add custom filters interceptors.Add(new CustomFilterInterceptor()); return interceptors; } } ``` ## See Also - [Dynamic Queries Overview](README.md) - [Queryable Providers](queryable-providers.md) - [Alter Queryable Services](alter-queryable-services.md) - [PoweredSoft.DynamicQuery Documentation](https://github.com/PoweredSoft/DynamicQuery)