14 KiB
14 KiB
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
public interface IDynamicQueryInterceptorProvider
{
List<IInterceptor> GetInterceptors<TSource, TDestination>(
IQueryable<TSource> queryable,
IDynamicQuery<TSource, TDestination> 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
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<Func<T, bool>> GetExpression<T>(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
public class CustomInterceptorProvider : IDynamicQueryInterceptorProvider
{
public List<IInterceptor> GetInterceptors<TSource, TDestination>(
IQueryable<TSource> queryable,
IDynamicQuery<TSource, TDestination> query)
{
return new List<IInterceptor>
{
new CustomFilterInterceptor()
};
}
}
Registration
builder.Services.AddDynamicQuery<Order, OrderDto>()
.AddDynamicQueryWithProvider<Order, OrderQueryableProvider>()
.AddDynamicQueryInterceptorProvider<Order, OrderDto, CustomInterceptorProvider>();
Usage
{
"filters": [
{
"path": "orderDate",
"operator": "Custom",
"customOperator": "IsWeekday"
}
]
}
Logging Interceptor
Query Logging
public class LoggingInterceptor : IBeforeQueryPageInterceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public void InterceptBeforeQueryPage<T>(IQueryable<T> 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<LoggingInterceptor> _logger;
public LoggingInterceptorProvider(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public List<IInterceptor> GetInterceptors<TSource, TDestination>(
IQueryable<TSource> queryable,
IDynamicQuery<TSource, TDestination> query)
{
return new List<IInterceptor>
{
new LoggingInterceptor(_logger)
};
}
}
Performance Monitoring
Query Performance Interceptor
public class PerformanceInterceptor : IBeforeQueryPageInterceptor
{
private readonly IMetricsCollector _metrics;
public PerformanceInterceptor(IMetricsCollector metrics)
{
_metrics = metrics;
}
public void InterceptBeforeQueryPage<T>(IQueryable<T> 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
public class DefaultFilterInterceptor : IFilterInterceptor
{
public bool CanHandle(IFilter filter)
{
// This interceptor handles all filters to add defaults
return true;
}
public Expression<Func<T, bool>> GetExpression<T>(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
public class CaseInsensitiveFilterInterceptor : IFilterInterceptor
{
public bool CanHandle(IFilter filter)
{
return filter is Filter f &&
f.Type == FilterType.String &&
f.Value is string;
}
public Expression<Func<T, bool>> GetExpression<T>(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
public class QueryValidationInterceptor : IBeforeQueryPageInterceptor
{
private const int MaxFilterCount = 10;
private const int MaxSortCount = 5;
public void InterceptBeforeQueryPage<T>(IQueryable<T> 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
public class CompositeInterceptorProvider : IDynamicQueryInterceptorProvider
{
private readonly ILogger<LoggingInterceptor> _logger;
private readonly IMetricsCollector _metrics;
public CompositeInterceptorProvider(
ILogger<LoggingInterceptor> logger,
IMetricsCollector metrics)
{
_logger = logger;
_metrics = metrics;
}
public List<IInterceptor> GetInterceptors<TSource, TDestination>(
IQueryable<TSource> queryable,
IDynamicQuery<TSource, TDestination> query)
{
return new List<IInterceptor>
{
new LoggingInterceptor(_logger),
new PerformanceInterceptor(_metrics),
new QueryValidationInterceptor(),
new CaseInsensitiveFilterInterceptor(),
new CustomFilterInterceptor()
};
}
}
Testing Interceptors
Unit Tests
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<Order>(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<Order>(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
public class ConditionalInterceptorProvider : IDynamicQueryInterceptorProvider
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<LoggingInterceptor> _logger;
public ConditionalInterceptorProvider(
IWebHostEnvironment environment,
ILogger<LoggingInterceptor> logger)
{
_environment = environment;
_logger = logger;
}
public List<IInterceptor> GetInterceptors<TSource, TDestination>(
IQueryable<TSource> queryable,
IDynamicQuery<TSource, TDestination> query)
{
var interceptors = new List<IInterceptor>();
// Only log in development
if (_environment.IsDevelopment())
{
interceptors.Add(new LoggingInterceptor(_logger));
}
// Always add custom filters
interceptors.Add(new CustomFilterInterceptor());
return interceptors;
}
}