Skip to content

Best Practices ​

This guide covers best practices for error handling and performance optimization when working with query strategies.

Error Handling ​

Handle errors gracefully when executing query strategies to provide a better user experience and make debugging easier.

Validation Before Execution ​

Validate query strategy parameters in the constructor to fail fast:

csharp
public class SearchProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto>
{
    private readonly string? _nameFilter;
    private readonly decimal? _minPrice;
    private readonly decimal? _maxPrice;

    public SearchProductsQueryStrategy(
        string? nameFilter = null,
        decimal? minPrice = null,
        decimal? maxPrice = null)
    {
        // Validate parameters
        if (minPrice.HasValue && minPrice.Value < 0)
        {
            throw new ArgumentException("Minimum price cannot be negative", nameof(minPrice));
        }

        if (maxPrice.HasValue && maxPrice.Value < 0)
        {
            throw new ArgumentException("Maximum price cannot be negative", nameof(maxPrice));
        }

        if (minPrice.HasValue && maxPrice.HasValue && minPrice.Value > maxPrice.Value)
        {
            throw new ArgumentException("Minimum price cannot be greater than maximum price");
        }

        _nameFilter = nameFilter;
        _minPrice = minPrice;
        _maxPrice = maxPrice;
    }

    protected override IQueryable<ProductDto> ExecuteCore(IQueryable<Product> source)
    {
        var query = source.AsQueryable();

        if (!string.IsNullOrWhiteSpace(_nameFilter))
        {
            query = query.Where(p => p.Name.Contains(_nameFilter));
        }

        if (_minPrice.HasValue)
        {
            query = query.Where(p => p.Price >= _minPrice.Value);
        }

        if (_maxPrice.HasValue)
        {
            query = query.Where(p => p.Price <= _maxPrice.Value);
        }

        return query.Select(p => new ProductDto
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price
        });
    }
}

Performance Considerations ​

Follow these guidelines to ensure your query strategies perform efficiently.

Avoid N+1 Queries ​

When working with related entities, be mindful of the N+1 query problem:

csharp
public class GetOrdersWithCustomersQueryStrategy : EntityQueryStrategy<Order, Order>
{
    protected override IQueryable<OrderDto> ExecuteCore(IQueryable<Order> source)
    {
        // This may cause N+1 queries if Customer is not loaded
        return source;
    }
}
csharp
public class GetOrdersWithCustomersQueryStrategy : EntityQueryStrategy<Order, OrderDto>
{
    protected override IQueryable<OrderDto> ExecuteCore(IQueryable<Order> source)
    {
        // Projection in Select avoids N+1 by translating to SQL JOIN
        return source.Select(o => new OrderDto
        {
            Id = o.Id,
            CustomerName = o.Customer.Name
        });
    }
}

Use Projections for Performance ​

Use projections to select only the data you need:

csharp
// Instead of returning entire entities
public class GetProductDetailsQueryStrategy : EntityQueryStrategy<Product, ProductDetailDto>
{
    protected override IQueryable<ProductDetailDto> ExecuteCore(IQueryable<Product> source)
    {
        // Project to DTO with only required fields
        return source.Select(p => new ProductDetailDto
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price,
            CategoryName = p.Category
            // Don't include heavy fields like images, descriptions unless needed
        });
    }
}

Limit Result Sets ​

Always consider limiting result sets for better performance:

csharp
public static class ProductQueryStrategy
{
    public static IEntityQueryStrategy<Product, ProductDto> GetTopSellingProducts(int count = 10) =>
        QueryStrategy.CreateForEntity(
            (IQueryable<Product> source) => source
                .Where(ProductSpecification.IsInStock)
                .OrderByDescending(p => p.SalesCount)
                .Take(count)
                .Select(p => new ProductDto
                {
                    Id = p.Id,
                    Name = p.Name,
                    Price = p.Price,
                    SalesCount = p.SalesCount
                }));
}

Optimize Filtering ​

Place the most restrictive filters first to reduce the dataset early:

csharp
public class GetRecentOrdersQueryStrategy : EntityQueryStrategy<Order, OrderDto>
{
    protected override IQueryable<OrderDto> ExecuteCore(IQueryable<Order> source)
    {
        // Apply most restrictive filter first
        return source
            .Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-7))  // Most restrictive
            .Where(o => o.Status == OrderStatus.Completed)           // Less restrictive
            .Where(o => o.TotalAmount > 0)                            // Least restrictive
            .OrderByDescending(o => o.CreatedAt)
            .Select(o => new OrderDto
            {
                Id = o.Id,
                TotalAmount = o.TotalAmount,
                CreatedAt = o.CreatedAt
            });
    }
}

Avoid Client-Side Evaluation ​

Ensure operations can be translated to SQL to avoid client-side evaluation:

csharp
public class GetProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto>
{
    protected override IQueryable<ProductDto> ExecuteCore(IQueryable<Product> source)
    {
        // Complex method call may not translate to SQL
        return source.Select(p => new ProductDto
        {
            Id = p.Id,
            DisplayName = FormatProductName(p.Name)  // Client-side evaluation!
        });
    }

    private string FormatProductName(string name) => /* complex logic */;
}
csharp
public class GetProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto>
{
    protected override IQueryable<ProductDto> ExecuteCore(IQueryable<Product> source)
    {
        // Simple operations that translate to SQL
        return source.Select(p => new ProductDto
        {
            Id = p.Id,
            DisplayName = p.Name.ToUpper()  // Translates to SQL UPPER()
        });
    }
}

Use Appropriate Indexes ​

Work with your database team to ensure proper indexes exist for commonly filtered and sorted columns:

csharp
// If this query is slow, ensure indexes exist on:
// - Product.Category
// - Product.Price
// - Product.AvailableQuantity
public class GetProductsByCategoryQueryStrategy : EntityQueryStrategy<Product, ProductDto>
{
    private readonly string _category;

    public GetProductsByCategoryQueryStrategy(string category)
    {
        _category = category;
    }

    protected override IQueryable<ProductDto> ExecuteCore(IQueryable<Product> source)
    {
        return source
            .Where(p => p.Category == _category)           // Index on Category
            .Where(p => p.AvailableQuantity > 0)           // Index on AvailableQuantity
            .OrderBy(p => p.Price)                         // Index on Price
            .Select(p => new ProductDto
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price
            });
    }
}

General Guidelines ​

1. Keep Query Strategies Focused ​

Each query strategy should have a single, well-defined purpose:

csharp
// Good - Single purpose
public class GetActiveProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto> { }
public class GetDiscontinuedProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto> { }

// Avoid - Multiple purposes
public class GetProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto>
{
    public GetProductsQueryStrategy(bool activeOnly, bool sortByPrice, bool includeOutOfStock)
    {
        // Too many responsibilities
    }
}

2. Make Query Strategies Immutable ​

Query strategy instances should be immutable after construction:

csharp
public class GetProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto>
{
    // No setters - immutable
    public required decimal MinPrice { get; init; } // Set when initializing only
}

3. Document Complex Queries ​

Add XML documentation to complex query strategies:

csharp
/// <summary>
/// Retrieves products that have been restocked within the specified number of days
/// and have a minimum quantity available. Results are ordered by restock date.
/// </summary>
/// <remarks>
/// This query joins Products with InventoryLogs to find recent restocking events.
/// Performance note: Ensure indexes exist on InventoryLogs.RestockDate and Product.AvailableQuantity.
/// </remarks>
public class GetRecentlyRestockedProductsQueryStrategy : EntityQueryStrategy<Product, ProductDto>
{
    // Implementation
}

Released under the MIT License.