dotnet-cqrs/docs/core-features/dynamic-queries/queryable-providers.md

557 lines
14 KiB
Markdown

# Queryable Providers
Implement data source providers for dynamic queries.
## Overview
Queryable providers (`IQueryableProvider<TSource>`) supply the base `IQueryable<TSource>` 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<TSource>
{
IQueryable<TSource> GetQueryable();
}
```
### Simple EF Core Provider
```csharp
public class ProductQueryableProvider : IQueryableProvider<Product>
{
private readonly ApplicationDbContext _context;
public ProductQueryableProvider(ApplicationDbContext context)
{
_context = context;
}
public IQueryable<Product> GetQueryable()
{
return _context.Products.AsNoTracking();
}
}
```
### Registration
```csharp
builder.Services.AddDynamicQuery<Product, ProductDto>()
.AddDynamicQueryWithProvider<Product, ProductQueryableProvider>();
```
## Advanced Patterns
### Provider with Default Filters
```csharp
public class ActiveProductQueryableProvider : IQueryableProvider<Product>
{
private readonly ApplicationDbContext _context;
public ActiveProductQueryableProvider(ApplicationDbContext context)
{
_context = context;
}
public IQueryable<Product> 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<Order>
{
private readonly ApplicationDbContext _context;
public OrderQueryableProvider(ApplicationDbContext context)
{
_context = context;
}
public IQueryable<Order> 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<Product>
{
private readonly ApplicationDbContext _context;
private readonly ITenantContext _tenantContext;
public TenantProductQueryableProvider(
ApplicationDbContext context,
ITenantContext tenantContext)
{
_context = context;
_tenantContext = tenantContext;
}
public IQueryable<Product> 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<Product>
{
private readonly ApplicationDbContext _context;
public IQueryable<Product> GetQueryable()
{
return _context.Products.AsNoTracking();
}
}
// Provider 2: Active products only (public use)
public class ActiveProductsQueryableProvider : IQueryableProvider<Product>
{
private readonly ApplicationDbContext _context;
public IQueryable<Product> GetQueryable()
{
return _context.Products
.AsNoTracking()
.Where(p => p.IsActive);
}
}
// Provider 3: Products in stock (sales use)
public class InStockProductsQueryableProvider : IQueryableProvider<Product>
{
private readonly ApplicationDbContext _context;
public IQueryable<Product> 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<Product, ProductDto>
{
public List<IFilter>? Filters { get; set; }
public List<ISort>? Sorts { get; set; }
}
builder.Services.AddDynamicQuery<Product, ProductDto, AdminProductDynamicQuery>()
.AddDynamicQueryWithProvider<Product, AllProductsQueryableProvider>();
// Public query - active products
public record PublicProductDynamicQuery : IDynamicQuery<Product, ProductDto>
{
public List<IFilter>? Filters { get; set; }
public List<ISort>? Sorts { get; set; }
}
builder.Services.AddDynamicQuery<Product, ProductDto, PublicProductDynamicQuery>()
.AddDynamicQueryWithProvider<Product, ActiveProductsQueryableProvider>();
```
## Performance Optimization
### AsNoTracking for Read-Only Queries
```csharp
public IQueryable<Product> GetQueryable()
{
// ✅ Good - No tracking overhead
return _context.Products.AsNoTracking();
// ❌ Bad - Unnecessary change tracking
// return _context.Products;
}
```
### Selective Includes
```csharp
public IQueryable<Order> 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<Product>
{
private readonly ApplicationDbContext _context;
public IQueryable<Product> 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<Product>
{
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<Product> GetQueryable()
{
const string cacheKey = "products_queryable";
if (!_cache.TryGetValue<List<Product>>(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<Order>
{
private readonly ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
public UserOrderQueryableProvider(
ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}
public IQueryable<Order> 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<Product>
{
private readonly ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
public RoleBasedProductQueryableProvider(
ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}
public IQueryable<Product> 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<Product>
{
private readonly List<Product> _products;
public MockProductQueryableProvider(List<Product> products)
{
_products = products;
}
public IQueryable<Product> GetQueryable()
{
return _products.AsQueryable();
}
}
// Unit test
[Fact]
public async Task DynamicQuery_FiltersProducts()
{
// Arrange
var products = new List<Product>
{
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<Product>
{
private readonly ApplicationDbContext _context;
public InMemoryProductQueryableProvider(ApplicationDbContext context)
{
_context = context;
}
public IQueryable<Product> GetQueryable()
{
return _context.Products.AsNoTracking();
}
}
// Test fixture
public class DynamicQueryTests : IDisposable
{
private readonly ApplicationDbContext _context;
public DynamicQueryTests()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.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<TSource> : IQueryableProvider<TSource>
where TSource : class, ITenantEntity
{
protected readonly ApplicationDbContext _context;
protected readonly ITenantContext _tenantContext;
protected SecureQueryableProvider(
ApplicationDbContext context,
ITenantContext tenantContext)
{
_context = context;
_tenantContext = tenantContext;
}
public IQueryable<TSource> GetQueryable()
{
var tenantId = _tenantContext.TenantId;
return _context.Set<TSource>()
.AsNoTracking()
.Where(e => e.TenantId == tenantId);
}
}
// Usage
public class ProductQueryableProvider : SecureQueryableProvider<Product>
{
public ProductQueryableProvider(
ApplicationDbContext context,
ITenantContext tenantContext)
: base(context, tenantContext)
{
}
}
```
### Pattern 2: Composite Provider
```csharp
public class CompositeProductQueryableProvider : IQueryableProvider<Product>
{
private readonly IEnumerable<IQueryableProvider<Product>> _providers;
public CompositeProductQueryableProvider(
IEnumerable<IQueryableProvider<Product>> providers)
{
_providers = providers;
}
public IQueryable<Product> GetQueryable()
{
IQueryable<Product> result = null;
foreach (var provider in _providers)
{
var queryable = provider.GetQueryable();
result = result == null ? queryable : result.Union(queryable);
}
return result ?? Enumerable.Empty<Product>().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)