Files
sql-utilities/docs/SqlUtilities.LinqToSql.md

883 lines
25 KiB
Markdown

# Strata.SqlTools.LinqToSql
**LINQ to SQL Query Analysis and Visualization**
---
## Overview
The `Strata.SqlTools.LinqToSql` package provides comprehensive support for analyzing LINQ to SQL queries by examining their expression trees. It extracts query components (SELECT, WHERE, ORDER BY, etc.) and provides visualization tools for understanding query structure and execution flow.
This package is particularly useful for:
- **Query Analysis**: Understanding how LINQ queries translate to SQL
- **Performance Optimization**: Identifying inefficient query patterns
- **Documentation**: Generating visual diagrams of query structure
- **Debugging**: Tracing LINQ method chains and their SQL equivalents
### Key Features
-**Expression Tree Analysis** - Parse IQueryable expression trees to extract SQL components
-**LINQ Method Chain Tracking** - Track Where, Select, OrderBy, GroupBy method calls
-**Statement Type Analysis** - Analyze INSERT, UPDATE, DELETE, PROCEDURE, and TRACE operations
-**Mermaid Diagram Generation** - Visualize queries with flowcharts and sequence diagrams
-**SQL Component Extraction** - Extract SELECT, WHERE, ORDER BY, GROUP BY clauses
-**Integration with SqlServer** - Built on top of SqlServer.QueryBreakdown
-**Type-Safe Analysis** - Strongly-typed entity detection
---
## Installation
```bash
dotnet add package Strata.SqlTools.LinqToSql
```
**Dependencies:**
- `Strata.SqlTools` (core functionality)
- `Strata.SqlTools.SqlServer` (base query breakdown)
- .NET 8.0+
---
## Quick Start
### Basic Query Analysis
```csharp
using Strata.SqlTools.Breakdowns.LinqToSql;
// Define your data context with IQueryable properties
public class DataContext
{
public IQueryable<User> Users => new List<User>().AsQueryable();
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public bool IsActive { get; set; }
}
// Analyze a LINQ query
var context = new DataContext();
var query = context.Users.Where(u => u.Age > 21).OrderBy(u => u.Name);
var breakdown = LinqQueryBreakdown.Analyze(query);
// Access extracted components
Console.WriteLine($"Entity Type: {breakdown.EntityType}");
Console.WriteLine($"SELECT: {breakdown.SelectClause}");
Console.WriteLine($"FROM: {breakdown.FromClause}");
Console.WriteLine($"WHERE: {breakdown.WhereClause}");
Console.WriteLine($"ORDER BY: {breakdown.OrderByClause}");
// Get method chain
var methodChain = breakdown.GetMethodChain();
Console.WriteLine($"Method Chain: {string.Join(" -> ", methodChain)}");
```
**Output:**
```
Entity Type: User
SELECT: *
FROM: Users
WHERE: (Age > 21)
ORDER BY: Name ASC
Method Chain: Where -> OrderBy
```
---
## Core Components
### LinqQueryBreakdown Class
The main class for analyzing LINQ queries.
#### Static Analysis Methods
```csharp
// SELECT Query Analysis
public static LinqQueryBreakdown Analyze<T>(IQueryable<T> query)
public static bool TryAnalyze<T>(IQueryable<T> query, out LinqQueryBreakdown? breakdown)
// INSERT Operations
public static InsertBreakdown AnalyzeInsert<T>(T entity) where T : class
public static InsertBreakdown AnalyzeInsertRange<T>(IEnumerable<T> entities) where T : class
// DELETE Operations
public static DeleteBreakdown AnalyzeDelete<T>(Expression<Func<T, bool>> filterExpression) where T : class
// UPDATE Operations
public static UpdateBreakdown AnalyzeUpdate<T>(
Expression<Func<T, bool>> filterExpression,
Expression<Func<T, T>> updateExpression) where T : class
// PROCEDURE Operations
public static ProcedureBreakdown AnalyzeProcedure(string procedureName, params object[] parameters)
// TRACE Operations
public static string AnalyzeTrace<T>(IQueryable<T> query, string? executionContext = null) where T : class
```
#### Properties
```csharp
public Expression? OriginalExpression { get; } // Original LINQ expression tree
public string EntityType { get; } // Entity type name (e.g., "User")
public List<string> MethodCallChain { get; } // List of LINQ method calls
```
#### Methods
```csharp
public string GetQuerySummary() // Human-readable query summary
public List<string> GetMethodChain() // LINQ method call sequence
```
---
## LinqExpressionVisitor
The expression visitor that traverses LINQ expression trees to extract SQL components.
### Supported LINQ Methods
| LINQ Method | SQL Clause | Example |
|-------------|------------|---------|
| `Where()` | WHERE | `users.Where(u => u.Age > 21)` |
| `Select()` | SELECT | `users.Select(u => new { u.Id, u.Name })` |
| `OrderBy()` | ORDER BY | `users.OrderBy(u => u.Name)` |
| `OrderByDescending()` | ORDER BY DESC | `users.OrderByDescending(u => u.Age)` |
| `GroupBy()` | GROUP BY | `users.GroupBy(u => u.Department)` |
| `ThenBy()` | ORDER BY (multiple) | `users.OrderBy(u => u.Name).ThenBy(u => u.Age)` |
### Expression Types Handled
- **Binary Expressions**: `>`, `<`, `>=`, `<=`, `==`, `!=`, `&&`, `||`
- **Member Access**: Property/field access (e.g., `u.Age`)
- **Constants**: Literal values
- **Method Calls**: LINQ extension methods
---
## Markdown Visualization
The `Strata.SqlTools.Markdown` package includes specialized generators for LinqToSql.
### Installation
```bash
dotnet add package Strata.SqlTools.Markdown
```
### QueryBreakdownGenerator
Generates Mermaid diagrams showing query structure and LINQ method chains.
```csharp
using Strata.SqlTools.Markdown.LinqToSql;
var generator = new QueryBreakdownGenerator();
var query = context.Users
.Where(u => u.Age > 21)
.OrderBy(u => u.Name)
.Select(u => new { u.Id, u.Name });
var breakdown = LinqQueryBreakdown.Analyze(query);
// Generate SQL structure diagram
string sqlDiagram = generator.GenerateMermaidDiagram(breakdown, "User Query");
// Generate LINQ method chain diagram
string methodDiagram = generator.GenerateMethodChainDiagram(breakdown, "Method Flow");
// Generate combined diagram (both SQL structure and method chain)
string combined = generator.GenerateCombinedDiagram(breakdown, "Complete Analysis");
```
**Example Method Chain Diagram:**
```mermaid
flowchart LR
Start[IQueryable] --> Where[Where]
Where --> OrderBy[OrderBy]
OrderBy --> Select[Select]
Select --> Result[Result]
```
### SqlStatementGenerator
Generates sequence diagrams showing LINQ execution pipeline.
```csharp
var stmtGenerator = new SqlStatementGenerator();
// Generate LINQ execution pipeline diagram
string pipeline = stmtGenerator.GenerateLinqPipelineDiagram(breakdown, "Query Execution");
// Generate sequence diagram
string sequence = stmtGenerator.GenerateSequenceDiagram(breakdown, "Execution Flow");
// Generate ER diagram
string erDiagram = stmtGenerator.GenerateEntityRelationshipDiagram(breakdown, "Entity Model");
```
**Example LINQ Pipeline Diagram:**
```mermaid
sequenceDiagram
participant Client as Client Application
participant LINQ as LINQ Provider
participant ET as Expression Tree
participant SQL as SQL Generator
participant DB as Database
Client->>LINQ: LINQ Query
activate LINQ
LINQ->>ET: Where Predicate
activate ET
LINQ->>ET: Select Projection
ET->>SQL: Expression Tree
deactivate ET
SQL->>DB: Generate SQL
activate DB
DB-->>SQL: Result Set
deactivate DB
SQL-->>LINQ: Mapped Objects
LINQ-->>Client: IEnumerable Result
deactivate LINQ
```
---
## Advanced Usage
### Complex Query Analysis
```csharp
// Multi-clause query
var complexQuery = context.Orders
.Where(o => o.Amount > 1000)
.Where(o => o.Status == "Pending")
.OrderBy(o => o.OrderDate)
.ThenByDescending(o => o.Amount)
.Select(o => new
{
o.Id,
o.CustomerName,
o.Amount
});
var breakdown = LinqQueryBreakdown.Analyze(complexQuery);
Console.WriteLine(breakdown.GetQuerySummary());
// Output: "SELECT projection FROM Orders WHERE (Amount > 1000) AND (Status = 'Pending') ORDER BY OrderDate ASC, Amount DESC"
var methods = breakdown.GetMethodChain();
// Output: ["Where", "Where", "OrderBy", "ThenByDescending", "Select"]
```
### Safe Analysis with TryAnalyze
```csharp
if (LinqQueryBreakdown.TryAnalyze(query, out var breakdown))
{
Console.WriteLine($"Successfully analyzed: {breakdown.GetQuerySummary()}");
// Access components safely
if (!string.IsNullOrEmpty(breakdown.WhereClause))
{
Console.WriteLine($"WHERE clause: {breakdown.WhereClause}");
}
}
else
{
Console.WriteLine("Unable to analyze query");
}
```
### Accessing Inherited SqlServer Properties
`LinqQueryBreakdown` inherits from `SqlServer.QueryBreakdown`, providing access to all standard query breakdown features:
```csharp
var breakdown = LinqQueryBreakdown.Analyze(query);
// Standard QueryBreakdown properties
Console.WriteLine($"SELECT: {breakdown.SelectClause}");
Console.WriteLine($"FROM: {breakdown.FromClause}");
Console.WriteLine($"WHERE: {breakdown.WhereClause}");
Console.WriteLine($"GROUP BY: {breakdown.GroupByClause}");
Console.WriteLine($"HAVING: {breakdown.HavingClause}");
Console.WriteLine($"ORDER BY: {breakdown.OrderByClause}");
// Generate SQL
string sql = breakdown.GetSql();
// Clone breakdown
var clone = (LinqQueryBreakdown)breakdown.Clone();
```
---
## Statement Type Analysis
Beyond SELECT queries, `LinqQueryBreakdown` provides comprehensive analysis for other statement types.
### INSERT Analysis
```csharp
// Single entity insert
var user = new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 };
var insertBreakdown = LinqQueryBreakdown.AnalyzeInsert(user);
Console.WriteLine($"Table: {insertBreakdown.TableName}"); // User
Console.WriteLine($"Columns: {insertBreakdown.InsertIntoClause}");
Console.WriteLine($"Values: {insertBreakdown.ValuesClause}");
// Bulk insert
var users = new List<User>
{
new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 },
new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 28 }
};
var bulkInsertBreakdown = LinqQueryBreakdown.AnalyzeInsertRange(users);
Console.WriteLine($"Inserting {bulkInsertBreakdown.ValuesClause.Count(c => c == '(')} rows");
```
### DELETE Analysis
```csharp
// Analyze deletion with filter expression
var deleteBreakdown = LinqQueryBreakdown.AnalyzeDelete<User>(u => u.Age < 18);
Console.WriteLine($"Table: {deleteBreakdown.FromClause}"); // User
Console.WriteLine($"WHERE: {deleteBreakdown.WhereClause}"); // (Age < 18)
// Complex filter
var complexDelete = LinqQueryBreakdown.AnalyzeDelete<Order>(o => o.Status == "Cancelled" && o.OrderDate < DateTime.Now.AddYears(-1));
Console.WriteLine($"Deleting old cancelled orders: {complexDelete.WhereClause}");
```
### UPDATE Analysis
```csharp
// Analyze update with filter and SET expressions
var updateBreakdown = LinqQueryBreakdown.AnalyzeUpdate<User>(
u => u.Department == "Sales",
u => new User { IsActive = false, UpdatedDate = DateTime.Now }
);
Console.WriteLine($"Table: {updateBreakdown.TableName}"); // User
Console.WriteLine($"WHERE: {updateBreakdown.WhereClause}"); // (Department = 'Sales')
Console.WriteLine($"SET: {updateBreakdown.SetClause}"); // Column assignments
// Practical example: Deactivate inactive users
var deactivateBreakdown = LinqQueryBreakdown.AnalyzeUpdate<User>(
u => u.LastLoginDate < DateTime.Now.AddDays(-90),
u => new User { IsActive = false }
);
```
### PROCEDURE Analysis
```csharp
// Simple procedure call
var procBreakdown = LinqQueryBreakdown.AnalyzeProcedure("sp_GetUsers");
Console.WriteLine($"Procedure: {procBreakdown.ProcedureName}");
Console.WriteLine($"Parameters: {procBreakdown.Parameters.Count}");
// Procedure with parameters
var procWithParamsBreakdown = LinqQueryBreakdown.AnalyzeProcedure(
"sp_GetUsersByAgeRange",
18, 65
);
Console.WriteLine($"Procedure: {procWithParamsBreakdown.ProcedureName}");
Console.WriteLine($"Parameter count: {procWithParamsBreakdown.Parameters.Count}");
foreach (var param in procWithParamsBreakdown.Parameters)
{
Console.WriteLine($" {param.Key}: {param.Value}");
}
```
### TRACE Analysis
```csharp
// Analyze query execution context
var query = _context.Users.Where(u => u.IsActive);
var traceInfo = LinqQueryBreakdown.AnalyzeTrace(query, "Initial User Load");
Console.WriteLine(traceInfo);
// Output:
// Trace Context for User
// Entity Type: Namespace.User
// Query Provider: EntityQueryProvider
// Expression: Where(Where(...))
// Execution Context: Initial User Load
// Timestamp: 2026-02-24T10:30:45.1234567Z
// Use in logging
_logger.LogInformation("Query trace:\n{Trace}", traceInfo);
```
---
## Use Cases
### 1. Query Performance Analysis
```csharp
var query = context.Products
.Where(p => p.Price > 100)
.Where(p => p.InStock)
.OrderBy(p => p.Name);
var breakdown = LinqQueryBreakdown.Analyze(query);
// Check for multiple WHERE clauses (could be combined)
var whereCount = breakdown.MethodCallChain.Count(m => m == "Where");
if (whereCount > 1)
{
Console.WriteLine($"Warning: {whereCount} separate WHERE clauses detected. Consider combining.");
}
```
### 2. Documentation Generation
```csharp
var queries = new Dictionary<string, IQueryable>
{
["ActiveUsers"] = context.Users.Where(u => u.IsActive),
["RecentOrders"] = context.Orders.Where(o => o.OrderDate > DateTime.Now.AddDays(-30)),
["TopProducts"] = context.Products.OrderByDescending(p => p.SalesCount).Take(10)
};
var generator = new QueryBreakdownGenerator();
var documentation = new StringBuilder();
foreach (var (name, query) in queries)
{
var breakdown = LinqQueryBreakdown.Analyze(query);
var diagram = generator.GenerateCombinedDiagram(breakdown, name);
documentation.AppendLine($"## {name}");
documentation.AppendLine(breakdown.GetQuerySummary());
documentation.AppendLine(diagram);
documentation.AppendLine();
}
File.WriteAllText("queries.md", documentation.ToString());
```
### 4. Data Modification Auditing
```csharp
public class AuditLogger
{
public void LogInsert<T>(T entity) where T : class
{
var breakdown = LinqQueryBreakdown.AnalyzeInsert(entity);
var audit = new AuditEntry
{
Operation = "INSERT",
Table = breakdown.TableName.Clause,
Columns = breakdown.InsertIntoClause.Clause,
Timestamp = DateTime.UtcNow
};
_auditContext.SaveAudit(audit);
}
public void LogDelete<T>(Expression<Func<T, bool>> filter) where T : class
{
var breakdown = LinqQueryBreakdown.AnalyzeDelete(filter);
var audit = new AuditEntry
{
Operation = "DELETE",
Table = breakdown.FromClause.Clause,
Condition = breakdown.WhereClause?.Clause,
Timestamp = DateTime.UtcNow
};
_auditContext.SaveAudit(audit);
}
public void LogUpdate<T>(Expression<Func<T, bool>> filter, Expression<Func<T, T>> updates) where T : class
{
var breakdown = LinqQueryBreakdown.AnalyzeUpdate(filter, updates);
var audit = new AuditEntry
{
Operation = "UPDATE",
Table = breakdown.TableName.Clause,
Updates = breakdown.SetClause?.Clause,
Condition = breakdown.WhereClause?.Clause,
Timestamp = DateTime.UtcNow
};
_auditContext.SaveAudit(audit);
}
}
```
### 5. Dynamic Query Logging
```csharp
public class QueryLogger
{
public void TraceExecution<T>(IQueryable<T> query, string context) where T : class
{
var traceInfo = LinqQueryBreakdown.AnalyzeTrace(query, context);
_logger.LogInformation("Query Execution Trace:\n{TraceInfo}", traceInfo);
if (LinqQueryBreakdown.TryAnalyze(query, out var breakdown))
{
_logger.LogDebug("Query Summary: {Summary}", breakdown.GetQuerySummary());
_logger.LogDebug("Methods: {Methods}", string.Join(" -> ", breakdown.GetMethodChain()));
}
}
}
// Usage
var query = _context.Users.Where(u => u.IsActive).OrderBy(u => u.Name);
_queryLogger.TraceExecution(query, "Active Users Report");
```
---
## Architecture
### Class Hierarchy
```
IQueryBreakdown (Interface)
QueryBreakdown (Strata.SqlTools)
SqlServer.QueryBreakdown
LinqQueryBreakdown
```
### Component Interaction
```mermaid
flowchart TD
A[IQueryable<T>] --> B[LinqQueryBreakdown.Analyze]
B --> C[LinqExpressionVisitor]
C --> D{Expression Type}
D -->|MethodCall| E[VisitMethodCall]
D -->|Binary| F[VisitBinary]
D -->|Member| G[VisitMember]
D -->|Constant| H[VisitConstant]
E --> I[Extract WHERE/SELECT/ORDER BY]
F --> I
G --> I
H --> I
I --> J[LinqQueryBreakdown Instance]
J --> K[QueryBreakdownGenerator]
K --> L[Mermaid Diagrams]
```
---
## Limitations
### Current Limitations
1. **Limited LINQ Method Support**: Currently supports Where, Select, OrderBy, OrderByDescending, ThenBy, GroupBy
- Not yet supported: Join, GroupJoin, Skip, Take, First, Last, etc.
2. **Simple Expressions Only**: Complex lambda expressions may not be fully parsed
- Example: Nested method calls in predicates
3. **No Subquery Analysis**: Subqueries in LINQ are not yet analyzed
4. **Entity Framework Specific**: Optimized for LINQ to SQL/Entity Framework patterns
- May not work with all IQueryable providers
5. **No Query Reconstruction**: The `GetQuery<T>()` method returns null because breakdowns are analyzed one-way
- Breakdown analysis cannot reconstruct the original LINQ query without the data provider
### What's Now Supported
**INSERT Analysis** - Extract column names and values from entity instances
**DELETE Analysis** - Extract filter conditions for deletion
**UPDATE Analysis** - Extract filter conditions and SET clauses
**PROCEDURE Analysis** - Parse procedure names and parameters
**TRACE Analysis** - Capture query execution context with timestamps
### Workarounds
For unsupported methods, you can still access the base `QueryBreakdown` properties:
```csharp
var query = context.Users.Take(10); // Take() not explicitly tracked
var breakdown = LinqQueryBreakdown.Analyze(query);
// SelectClause, FromClause still available
// MethodCallChain may be incomplete
```
For query reconstruction, use the original IQueryable directly rather than attempting to reconstruct from the breakdown.
---
## Best Practices
### 1. Use TryAnalyze for Dynamic Queries
```csharp
// Good: Handle analysis failures gracefully
if (LinqQueryBreakdown.TryAnalyze(userProvidedQuery, out var breakdown))
{
ProcessBreakdown(breakdown);
}
else
{
LogError("Unable to analyze query");
}
// Avoid: Analyze() throws on failure
var breakdown = LinqQueryBreakdown.Analyze(userProvidedQuery); // May throw
```
### 2. Check for Null Components
```csharp
var breakdown = LinqQueryBreakdown.Analyze(query);
// Good: Check before using
if (!string.IsNullOrEmpty(breakdown.WhereClause))
{
Console.WriteLine($"Filter: {breakdown.WhereClause}");
}
// Avoid: Direct access without checking
Console.WriteLine(breakdown.WhereClause.Length); // NullReferenceException if no WHERE
```
### 3. Combine with Logging
```csharp
public IQueryable<User> GetFilteredUsers(int minAge)
{
var query = _context.Users.Where(u => u.Age >= minAge);
// Log query structure for debugging
if (LinqQueryBreakdown.TryAnalyze(query, out var breakdown))
{
_logger.LogDebug("Query: {Summary}", breakdown.GetQuerySummary());
_logger.LogDebug("Methods: {Methods}", string.Join(", ", breakdown.GetMethodChain()));
}
return query;
}
```
---
## Testing
The LinqToSql package includes comprehensive unit tests for all analysis types:
### SELECT Query Tests
```csharp
[Test]
public void Analyze_SimpleSelectQuery_ExtractsTableName()
{
var query = _context.Users;
var breakdown = LinqQueryBreakdown.Analyze(query);
Assert.That(breakdown.EntityType, Is.EqualTo("User"));
Assert.That(breakdown.FromClause, Is.EqualTo("Users"));
}
[Test]
public void Analyze_WhereClause_ExtractsCondition()
{
var query = _context.Users.Where(u => u.Age > 21);
var breakdown = LinqQueryBreakdown.Analyze(query);
Assert.That(breakdown.WhereClause, Does.Contain("Age"));
Assert.That(breakdown.WhereClause, Does.Contain(">"));
Assert.That(breakdown.WhereClause, Does.Contain("21"));
}
[Test]
public void GetMethodChain_MultipleOperations_ReturnsCorrectSequence()
{
var query = _context.Users
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.Select(u => new { u.Id, u.Name });
var breakdown = LinqQueryBreakdown.Analyze(query);
var chain = breakdown.GetMethodChain();
Assert.That(chain, Is.EqualTo(new[] { "Where", "OrderBy", "Select" }));
}
```
### Statement Type Tests
```csharp
[Test]
public void AnalyzeInsert_SingleEntity_CreatesInsertBreakdown()
{
var entity = new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 };
var breakdown = LinqQueryBreakdown.AnalyzeInsert(entity);
Assert.That(breakdown.TableName.Clause, Is.EqualTo("User"));
Assert.That(breakdown.InsertIntoClause.Clause, Does.Contain("Id"));
Assert.That(breakdown.InsertIntoClause.Clause, Does.Contain("Name"));
}
[Test]
public void AnalyzeInsertRange_MultipleEntities_CreatesInsertBreakdown()
{
var entities = new List<User>
{
new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 },
new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 28 }
};
var breakdown = LinqQueryBreakdown.AnalyzeInsertRange(entities);
Assert.That(breakdown.TableName.Clause, Is.EqualTo("User"));
Assert.That(breakdown.ValuesClause.Clause, Does.Contain("("));
}
[Test]
public void AnalyzeDelete_WithFilterExpression_CreatesDeleteBreakdown()
{
var breakdown = LinqQueryBreakdown.AnalyzeDelete<User>(u => u.Age < 18);
Assert.That(breakdown.FromClause.Clause, Is.EqualTo("User"));
Assert.That(breakdown.WhereClause.Clause, Is.Not.Empty);
}
[Test]
public void AnalyzeUpdate_WithFilterAndUpdateExpressions_CreatesUpdateBreakdown()
{
var breakdown = LinqQueryBreakdown.AnalyzeUpdate<User>(
u => u.Department == "Sales",
u => new User { IsActive = false }
);
Assert.That(breakdown.TableName.Clause, Is.EqualTo("User"));
Assert.That(breakdown.WhereClause.Clause, Is.Not.Empty);
}
[Test]
public void AnalyzeProcedure_WithName_CreatesProcedureBreakdown()
{
var breakdown = LinqQueryBreakdown.AnalyzeProcedure("sp_GetUsers");
Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("sp_GetUsers"));
}
[Test]
public void AnalyzeProcedure_WithParameters_CreatesProcedureBreakdownWithParams()
{
var breakdown = LinqQueryBreakdown.AnalyzeProcedure("sp_GetUsersByAge", 18, 65);
Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("sp_GetUsersByAge"));
Assert.That(breakdown.Parameters.Count, Is.EqualTo(2));
}
[Test]
public void AnalyzeTrace_WithValidQuery_ReturnsTraceString()
{
var query = _context.Users.Where(u => u.Age > 18);
var trace = LinqQueryBreakdown.AnalyzeTrace(query, "Test Context");
Assert.That(trace, Does.Contain("User"));
Assert.That(trace, Does.Contain("Query Provider"));
Assert.That(trace, Does.Contain("Test Context"));
}
```
---
## Troubleshooting
### Query Analysis Returns Empty Results
**Problem**: `LinqQueryBreakdown.Analyze()` returns a breakdown with null/empty clauses.
**Solution**: Ensure your query is an `IQueryable<T>`. LINQ to Objects (`IEnumerable<T>`) won't work:
```csharp
// Wrong: IEnumerable (LINQ to Objects)
var list = new List<User>();
var query = list.Where(u => u.Age > 21); // IEnumerable<User>
// Right: IQueryable (LINQ to SQL)
var query = _context.Users.Where(u => u.Age > 21); // IQueryable<User>
```
### Method Chain Missing Methods
**Problem**: `GetMethodChain()` doesn't show all LINQ methods used.
**Solution**: Only supported methods are tracked. Check the [Supported LINQ Methods](#supported-linq-methods) table.
### Expression Too Complex
**Problem**: Complex lambda expressions aren't fully parsed.
**Solution**: Simplify expressions or break into multiple LINQ calls:
```csharp
// Complex (may not parse fully)
var query = users.Where(u => CalculateScore(u.Age, u.Experience) > threshold);
// Simpler (parses better)
var query = users.Where(u => u.Age > minAge).Where(u => u.Experience > minExp);
```
---
## API Reference
### Namespaces
- `Strata.SqlTools.Breakdowns.LinqToSql` - Core breakdown classes
- `Strata.SqlTools.Visitors.LinqToSql` - Expression tree visitors
- `Strata.SqlTools.Markdown.LinqToSql` - Markdown/Mermaid generators
### Key Classes
| Class | Purpose |
|-------|---------|
| `LinqQueryBreakdown` | Main analysis class, analyzes IQueryable expressions |
| `LinqExpressionVisitor` | Expression tree visitor for extracting SQL components |
| `QueryBreakdownGenerator` | Generates Mermaid diagrams from breakdowns |
| `SqlStatementGenerator` | Generates sequence/pipeline diagrams |
---
## Related Documentation
- [SqlUtilities.Core.md](SqlUtilities.Core.md) - Core library documentation
- [SqlUtilities.SqlServer.md](SqlUtilities.SqlServer.md) - SQL Server base classes
- [EFCore_Integration_Guide.md](EFCore_Integration_Guide.md) - Entity Framework Core integration
---
**Version**: 1.1.0
**Last Updated**: February 2026
**Package**: Strata.SqlTools.LinqToSql
**Changelog**:
- v1.1.0: Added statement type analysis methods (INSERT, UPDATE, DELETE, PROCEDURE, TRACE)
- v1.0.0: Initial release with SELECT query analysis