# Queryable Providers Implement data source providers for dynamic queries. ## Overview Queryable providers (`IQueryableProvider`) supply the base `IQueryable` that dynamic queries operate on. They encapsulate data source logic and enable: - ✅ **Data source abstraction** - Hide EF Core, Dapper, or other data access - ✅ **Default filtering** - Apply base filters to all queries - ✅ **Performance optimization** - Configure tracking, includes, indexes - ✅ **Multiple sources** - Different providers for different scenarios - ✅ **Testability** - Mock providers for unit tests ## Basic Provider ### IQueryableProvider Interface ```csharp public interface IQueryableProvider { IQueryable GetQueryable(); } ``` ### Simple EF Core Provider ```csharp public class ProductQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public ProductQueryableProvider(ApplicationDbContext context) { _context = context; } public IQueryable GetQueryable() { return _context.Products.AsNoTracking(); } } ``` ### Registration ```csharp builder.Services.AddDynamicQuery() .AddDynamicQueryWithProvider(); ``` ## Advanced Patterns ### Provider with Default Filters ```csharp public class ActiveProductQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public ActiveProductQueryableProvider(ApplicationDbContext context) { _context = context; } public IQueryable GetQueryable() { return _context.Products .AsNoTracking() .Where(p => p.IsActive) // Only active products .Where(p => p.DeletedAt == null); // Not soft-deleted } } ``` ### Provider with Includes ```csharp public class OrderQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public OrderQueryableProvider(ApplicationDbContext context) { _context = context; } public IQueryable GetQueryable() { return _context.Orders .AsNoTracking() .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(i => i.Product); } } ``` ### Provider with Tenant Filtering ```csharp public class TenantProductQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; private readonly ITenantContext _tenantContext; public TenantProductQueryableProvider( ApplicationDbContext context, ITenantContext tenantContext) { _context = context; _tenantContext = tenantContext; } public IQueryable GetQueryable() { var tenantId = _tenantContext.TenantId; return _context.Products .AsNoTracking() .Where(p => p.TenantId == tenantId); } } ``` ## Multiple Providers ### Scenario: Different Providers for Different Use Cases ```csharp // Provider 1: All products (admin use) public class AllProductsQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public IQueryable GetQueryable() { return _context.Products.AsNoTracking(); } } // Provider 2: Active products only (public use) public class ActiveProductsQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public IQueryable GetQueryable() { return _context.Products .AsNoTracking() .Where(p => p.IsActive); } } // Provider 3: Products in stock (sales use) public class InStockProductsQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public IQueryable GetQueryable() { return _context.Products .AsNoTracking() .Where(p => p.IsActive) .Where(p => p.Stock > 0); } } ``` ### Registration with Different Queries ```csharp // Admin query - all products public record AdminProductDynamicQuery : IDynamicQuery { public List? Filters { get; set; } public List? Sorts { get; set; } } builder.Services.AddDynamicQuery() .AddDynamicQueryWithProvider(); // Public query - active products public record PublicProductDynamicQuery : IDynamicQuery { public List? Filters { get; set; } public List? Sorts { get; set; } } builder.Services.AddDynamicQuery() .AddDynamicQueryWithProvider(); ``` ## Performance Optimization ### AsNoTracking for Read-Only Queries ```csharp public IQueryable GetQueryable() { // ✅ Good - No tracking overhead return _context.Products.AsNoTracking(); // ❌ Bad - Unnecessary change tracking // return _context.Products; } ``` ### Selective Includes ```csharp public IQueryable GetQueryable() { // ✅ Good - Include only what's needed in DTO return _context.Orders .AsNoTracking() .Include(o => o.Customer); // ❌ Bad - Unnecessary includes // return _context.Orders // .Include(o => o.Customer) // .Include(o => o.Items) // Not needed in DTO // .Include(o => o.ShippingAddress) // Not needed in DTO // .Include(o => o.BillingAddress); // Not needed in DTO } ``` ### Projections in Provider ```csharp public class ProductSummaryQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public IQueryable GetQueryable() { // Only select columns needed for the DTO return _context.Products .AsNoTracking() .Select(p => new Product { Id = p.Id, Name = p.Name, Price = p.Price, Category = p.Category // Omit large columns like Description, Images, etc. }); } } ``` ## Caching Strategies ### In-Memory Caching ```csharp public class CachedProductQueryableProvider : IQueryableProvider { private readonly IMemoryCache _cache; private readonly ApplicationDbContext _context; private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); public CachedProductQueryableProvider( IMemoryCache cache, ApplicationDbContext context) { _cache = cache; _context = context; } public IQueryable GetQueryable() { const string cacheKey = "products_queryable"; if (!_cache.TryGetValue>(cacheKey, out var products)) { products = _context.Products .AsNoTracking() .ToList(); _cache.Set(cacheKey, products, CacheDuration); } return products.AsQueryable(); } } ``` **Note:** Only cache for small, relatively static datasets. Large or frequently changing data should not be cached this way. ## User-Specific Providers ### Current User's Data ```csharp public class UserOrderQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; private readonly IHttpContextAccessor _httpContextAccessor; public UserOrderQueryableProvider( ApplicationDbContext context, IHttpContextAccessor httpContextAccessor) { _context = context; _httpContextAccessor = httpContextAccessor; } public IQueryable GetQueryable() { var userId = _httpContextAccessor.HttpContext?.User .FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userId)) { return _context.Orders .AsNoTracking() .Where(o => false); // No results for unauthenticated users } return _context.Orders .AsNoTracking() .Where(o => o.UserId == userId); } } ``` ### Role-Based Filtering ```csharp public class RoleBasedProductQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; private readonly IHttpContextAccessor _httpContextAccessor; public RoleBasedProductQueryableProvider( ApplicationDbContext context, IHttpContextAccessor httpContextAccessor) { _context = context; _httpContextAccessor = httpContextAccessor; } public IQueryable GetQueryable() { var user = _httpContextAccessor.HttpContext?.User; if (user == null) return _context.Products.Where(p => false); var query = _context.Products.AsNoTracking(); // Admins see all products if (user.IsInRole("Admin")) return query; // Managers see active products if (user.IsInRole("Manager")) return query.Where(p => p.IsActive); // Regular users see active, in-stock products return query .Where(p => p.IsActive) .Where(p => p.Stock > 0); } } ``` ## Testing ### Mock Queryable Provider ```csharp public class MockProductQueryableProvider : IQueryableProvider { private readonly List _products; public MockProductQueryableProvider(List products) { _products = products; } public IQueryable GetQueryable() { return _products.AsQueryable(); } } // Unit test [Fact] public async Task DynamicQuery_FiltersProducts() { // Arrange var products = new List { new() { Id = 1, Name = "Laptop", Category = "Electronics", Price = 999 }, new() { Id = 2, Name = "Book", Category = "Books", Price = 20 }, new() { Id = 3, Name = "Mouse", Category = "Electronics", Price = 25 } }; var provider = new MockProductQueryableProvider(products); // Use provider in tests... } ``` ### In-Memory EF Core Provider ```csharp public class InMemoryProductQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public InMemoryProductQueryableProvider(ApplicationDbContext context) { _context = context; } public IQueryable GetQueryable() { return _context.Products.AsNoTracking(); } } // Test fixture public class DynamicQueryTests : IDisposable { private readonly ApplicationDbContext _context; public DynamicQueryTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; _context = new ApplicationDbContext(options); // Seed test data _context.Products.AddRange( new Product { Id = 1, Name = "Test Product 1" }, new Product { Id = 2, Name = "Test Product 2" } ); _context.SaveChanges(); } [Fact] public void Provider_ReturnsExpectedProducts() { var provider = new InMemoryProductQueryableProvider(_context); var queryable = provider.GetQueryable(); Assert.Equal(2, queryable.Count()); } public void Dispose() { _context.Dispose(); } } ``` ## Common Patterns ### Pattern 1: Base Query with Security ```csharp public abstract class SecureQueryableProvider : IQueryableProvider where TSource : class, ITenantEntity { protected readonly ApplicationDbContext _context; protected readonly ITenantContext _tenantContext; protected SecureQueryableProvider( ApplicationDbContext context, ITenantContext tenantContext) { _context = context; _tenantContext = tenantContext; } public IQueryable GetQueryable() { var tenantId = _tenantContext.TenantId; return _context.Set() .AsNoTracking() .Where(e => e.TenantId == tenantId); } } // Usage public class ProductQueryableProvider : SecureQueryableProvider { public ProductQueryableProvider( ApplicationDbContext context, ITenantContext tenantContext) : base(context, tenantContext) { } } ``` ### Pattern 2: Composite Provider ```csharp public class CompositeProductQueryableProvider : IQueryableProvider { private readonly IEnumerable> _providers; public CompositeProductQueryableProvider( IEnumerable> providers) { _providers = providers; } public IQueryable GetQueryable() { IQueryable result = null; foreach (var provider in _providers) { var queryable = provider.GetQueryable(); result = result == null ? queryable : result.Union(queryable); } return result ?? Enumerable.Empty().AsQueryable(); } } ``` ## Best Practices ### ✅ DO - Use AsNoTracking() for read-only queries - Filter at the provider level for security (tenant isolation) - Include only necessary related entities - Use dependency injection for context and services - Create separate providers for different use cases - Test providers independently ### ❌ DON'T - Don't track entities in providers - Don't perform synchronous operations (ToList, Count) - Don't include unnecessary related entities - Don't cache large datasets in memory - Don't skip security filters - Don't expose internal implementation details ## See Also - [Dynamic Queries Overview](README.md) - [Getting Started](getting-started.md) - [Alter Queryable Services](alter-queryable-services.md) - [Interceptors](interceptors.md)