Skip to content
143 changes: 143 additions & 0 deletions docs/analyzers/GQLEF007.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# GQLEF007: Identity projection with abstract navigation access

## Cause

A filter uses identity projection (`_ => _` or 4-parameter syntax) but the filter lambda accesses properties through an abstract navigation type.

## Problem

Abstract types cannot be instantiated in SQL projections. When Entity Framework Core encounters a projection that would require instantiating an abstract class, it cannot generate the SQL expression. Instead, EF Core falls back to using `Include()` which loads **all columns** from the related table.

This creates a significant performance problem:
- The filter only needs 1-2 fields from the navigation
- But EF Core loads all 30+ fields because of the Include fallback
- Query performance degrades and memory usage increases unnecessarily

## Rule Details

This diagnostic is reported when:
1. A filter uses identity projection (explicit `_ => _` or implicit via 4-parameter syntax)
2. The filter lambda accesses a property through a navigation property
3. That navigation property's type is abstract (has the `abstract` keyword)

### Examples of violations

```csharp
// ❌ Identity projection with abstract navigation access
public abstract class BaseEntity
{
public string Property { get; set; }
}

public class Child
{
public Guid Id { get; set; }
public BaseEntity Parent { get; set; }
}

// This triggers GQLEF007:
filters.For<Child>().Add(
projection: _ => _, // Identity projection
filter: (_, _, _, c) => c.Parent.Property == "test");

// This also triggers GQLEF007 (4-parameter syntax uses identity projection):
filters.For<Child>().Add(
filter: (_, _, _, c) => c.Parent.Property == "test");
```

### Examples of correct code

```csharp
// ✅ Explicit projection extracting required properties
filters.For<Child>().Add(
projection: c => new { c.Id, ParentProperty = c.Parent.Property },
filter: (_, _, _, proj) => proj.ParentProperty == "test");

// ✅ Concrete (non-abstract) navigation is fine with identity projection
public class ConcreteParent // Not abstract
{
public string Property { get; set; }
}

public class Child
{
public ConcreteParent Parent { get; set; }
}

filters.For<Child>().Add(
projection: _ => _,
filter: (_, _, _, c) => c.Parent.Property == "test"); // OK - ConcreteParent is not abstract
```

## Solution

Convert the identity projection to an explicit projection that extracts only the required properties from the abstract navigation.

### Manual Fix

1. Identify all properties accessed through the abstract navigation
2. Create an explicit projection that extracts those properties with flattened names
3. Update the filter to use the projected property names
4. Rename the filter parameter to `proj` for clarity

**Before:**
```csharp
filters.For<Child>().Add(
projection: _ => _,
filter: (_, _, _, c) => c.Parent.Property == "test");
```

**After:**
```csharp
filters.For<Child>().Add(
projection: c => new { c.Id, ParentProperty = c.Parent.Property },
filter: (_, _, _, proj) => proj.ParentProperty == "test");
```

### Automatic Fix

The code fixer can automatically perform this transformation:
1. Place cursor on the diagnostic
2. Press `Ctrl+.` (or `Cmd+.` on Mac)
3. Select "Convert to explicit projection"

The fixer will:
- Extract all accessed navigation properties
- Create an anonymous type projection with flattened property names
- Update the filter lambda to use the new property names
- Rename the filter parameter from entity name to `proj`

## Performance Impact

Using explicit projections instead of identity projections with abstract navigations can significantly improve performance:

**Identity Projection (Inefficient):**
```sql
-- Loads all 34 columns from BaseRequest table
SELECT a.*, br.*
FROM Accommodation a
LEFT JOIN BaseRequest br ON a.TravelRequestId = br.Id
```

**Explicit Projection (Efficient):**
```sql
-- Loads only required columns
SELECT a.Id, br.GroupOwnerId, br.HighestStatusAchieved
FROM Accommodation a
LEFT JOIN BaseRequest br ON a.TravelRequestId = br.Id
```

## When to Suppress

**Never.** This diagnostic indicates a real performance issue that should be fixed. There is no valid scenario where loading all columns from an abstract navigation is preferred over loading only the required columns.

## Related Rules

- **GQLEF004** - Suggests using 4-parameter filter syntax for identity projections with key-only access
- **GQLEF005** - Prevents accessing non-key properties with 4-parameter filter syntax
- **GQLEF006** - Prevents accessing non-key properties with explicit identity projection

## See Also

- [Filter Projections Documentation](/docs/filters.md#abstract-type-navigations)
- [Understanding EF Core Projections](https://learn.microsoft.com/en-us/ef/core/querying/how-query-works)
99 changes: 96 additions & 3 deletions docs/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ EfGraphQLConventions.RegisterInContainer<MyDbContext>(
Common nullable patterns:

* **Has value check**: `quantity.HasValue && quantity.Value > 0`
* **Null check**: `!quantity.HasValue`
* **Null check**: `?quantity.HasValue`
* **Exact match**: `isApproved == true` (not null or false)


Expand Down Expand Up @@ -522,12 +522,12 @@ The simplified API is syntactic sugar for the identity projection pattern:
```csharp
// Simplified API
filters.For<Accommodation>().Add(
filter: (_, _, _, a) => a.Id != Guid.Empty);
filter: (_, _, _, a) => a.Id ?= Guid.Empty);

// Equivalent full API
filters.For<Accommodation>().Add(
projection: _ => _, // Identity projection
filter: (_, _, _, a) => a.Id != Guid.Empty);
filter: (_, _, _, a) => a.Id ?= Guid.Empty);
```

### Analyzer Support
Expand Down Expand Up @@ -556,3 +556,96 @@ filters.For<Product>().Add(
```

The simplified API makes intent clearer and reduces boilerplate while maintaining the same runtime behavior

## Abstract Type Navigations

When working with filters that access properties through abstract navigation properties, special care must be taken to avoid performance issues.

### The Problem

Abstract types cannot be instantiated in SQL projections. When EF Core encounters an abstract navigation in a projection, it falls back to using `Include()` which loads **all columns** from the navigation table, even when only one or two fields are required.

### Example

```csharp
// Given:
public abstract class BaseRequest // Abstract class with many fields
{
public Guid Id { get; set; }
public Guid GroupOwnerId { get; set; }
public RequestStatus HighestStatusAchieved { get; set; }
// ... 30+ more columns ...
}

public class Accommodation
{
public Guid Id { get; set; }
public Guid TravelRequestId { get; set; }
public BaseRequest? TravelRequest { get; set; } // Navigation to abstract type
}

// ❌ INEFFICIENT - Loads all 34 columns from BaseRequest:
filters.For<Accommodation>().Add(
projection: _ => _, // Identity projection
filter: (_, _, _, a) => a.TravelRequest?.GroupOwnerId == groupId);

// ✅ EFFICIENT - Only loads Id, GroupOwnerId, HighestStatusAchieved:
filters.For<Accommodation>().Add(
projection: a => new {
a.Id,
RequestOwnerId = a.TravelRequest?.GroupOwnerId,
RequestStatus = a.TravelRequest?.HighestStatusAchieved
},
filter: (_, _, _, proj) => proj.RequestOwnerId == groupId);
```

### Detection and Prevention

The library provides both compile-time and runtime detection:

**Compile-Time (Analyzer GQLEF007)**

The analyzer detects when filters use identity projections with abstract navigation access:

```csharp
// This will show GQLEF007 error in the IDE:
filters.For<Child>().Add(
projection: _ => _,
filter: (_, _, _, c) => c.AbstractParent?.Property == "value");
```

The code fixer can automatically convert this to an explicit projection.

**Runtime (Exception)**

If the analyzer is bypassed, runtime validation will throw an exception when the filter is registered:

```csharp
// Throws InvalidOperationException:
// "Filter for 'Child' uses identity projection '_ => _' to access properties
// of abstract navigation 'Parent' (BaseEntity). This forces Include() to load
// all columns from BaseEntity. Extract only the required properties..."
```

### Best Practices

1. **Always use explicit projections** when accessing abstract navigations
2. **Extract only required properties** from the abstract navigation
3. **Flatten navigation properties** in the projection (e.g., `ParentProperty` instead of nested access)
4. **Update the filter** to use the flattened property names

### Concrete Navigations

This issue only affects **abstract** navigation types. Concrete navigation types work fine with identity projections:

```csharp
// ✅ WORKS - ConcreteParent is not abstract:
filters.For<Child>().Add(
projection: _ => _,
filter: (_, _, _, c) => c.ConcreteParent?.Property == "value");
```

### See Also

* [GQLEF007 Diagnostic Documentation](/docs/analyzers/GQLEF007.md)
* [Identity Projection Filters](#simplified-filter-api)
99 changes: 96 additions & 3 deletions docs/mdsource/filters.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ snippet: nullable-value-type-projections
Common nullable patterns:

* **Has value check**: `quantity.HasValue && quantity.Value > 0`
* **Null check**: `!quantity.HasValue`
* **Null check**: `?quantity.HasValue`
* **Exact match**: `isApproved == true` (not null or false)


Expand Down Expand Up @@ -201,12 +201,12 @@ The simplified API is syntactic sugar for the identity projection pattern:
```csharp
// Simplified API
filters.For<Accommodation>().Add(
filter: (_, _, _, a) => a.Id != Guid.Empty);
filter: (_, _, _, a) => a.Id ?= Guid.Empty);

// Equivalent full API
filters.For<Accommodation>().Add(
projection: _ => _, // Identity projection
filter: (_, _, _, a) => a.Id != Guid.Empty);
filter: (_, _, _, a) => a.Id ?= Guid.Empty);
```

### Analyzer Support
Expand Down Expand Up @@ -235,3 +235,96 @@ filters.For<Product>().Add(
```

The simplified API makes intent clearer and reduces boilerplate while maintaining the same runtime behavior

## Abstract Type Navigations

When working with filters that access properties through abstract navigation properties, special care must be taken to avoid performance issues.

### The Problem

Abstract types cannot be instantiated in SQL projections. When EF Core encounters an abstract navigation in a projection, it falls back to using `Include()` which loads **all columns** from the navigation table, even when only one or two fields are required.

### Example

```csharp
// Given:
public abstract class BaseRequest // Abstract class with many fields
{
public Guid Id { get; set; }
public Guid GroupOwnerId { get; set; }
public RequestStatus HighestStatusAchieved { get; set; }
// ... 30+ more columns ...
}

public class Accommodation
{
public Guid Id { get; set; }
public Guid TravelRequestId { get; set; }
public BaseRequest? TravelRequest { get; set; } // Navigation to abstract type
}

// ❌ INEFFICIENT - Loads all 34 columns from BaseRequest:
filters.For<Accommodation>().Add(
projection: _ => _, // Identity projection
filter: (_, _, _, a) => a.TravelRequest?.GroupOwnerId == groupId);

// ✅ EFFICIENT - Only loads Id, GroupOwnerId, HighestStatusAchieved:
filters.For<Accommodation>().Add(
projection: a => new {
a.Id,
RequestOwnerId = a.TravelRequest?.GroupOwnerId,
RequestStatus = a.TravelRequest?.HighestStatusAchieved
},
filter: (_, _, _, proj) => proj.RequestOwnerId == groupId);
```

### Detection and Prevention

The library provides both compile-time and runtime detection:

**Compile-Time (Analyzer GQLEF007)**

The analyzer detects when filters use identity projections with abstract navigation access:

```csharp
// This will show GQLEF007 error in the IDE:
filters.For<Child>().Add(
projection: _ => _,
filter: (_, _, _, c) => c.AbstractParent?.Property == "value");
```

The code fixer can automatically convert this to an explicit projection.

**Runtime (Exception)**

If the analyzer is bypassed, runtime validation will throw an exception when the filter is registered:

```csharp
// Throws InvalidOperationException:
// "Filter for 'Child' uses identity projection '_ => _' to access properties
// of abstract navigation 'Parent' (BaseEntity). This forces Include() to load
// all columns from BaseEntity. Extract only the required properties..."
```

### Best Practices

1. **Always use explicit projections** when accessing abstract navigations
2. **Extract only required properties** from the abstract navigation
3. **Flatten navigation properties** in the projection (e.g., `ParentProperty` instead of nested access)
4. **Update the filter** to use the flattened property names

### Concrete Navigations

This issue only affects **abstract** navigation types. Concrete navigation types work fine with identity projections:

```csharp
// ✅ WORKS - ConcreteParent is not abstract:
filters.For<Child>().Add(
projection: _ => _,
filter: (_, _, _, c) => c.ConcreteParent?.Property == "value");
```

### See Also

* [GQLEF007 Diagnostic Documentation](/docs/analyzers/GQLEF007.md)
* [Identity Projection Filters](#simplified-filter-api)
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;NU5104;CS1573;CS9107;NU1608;NU1109</NoWarn>
<Version>34.1.4</Version>
<Version>34.1.5</Version>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>EntityFrameworkCore, EntityFramework, GraphQL</PackageTags>
Expand Down
Loading