Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions docs/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,152 @@ This is useful when:
* User permissions are stored in the database
* Filter logic requires async operations
* Complex checks involve multiple data sources


## Simplified Filter API

For filters that only need to access **primary key** or **foreign key** properties, a simplified API is available that eliminates the need to specify a projection:

<!-- snippet: simplified-filter-api -->
<a id='snippet-simplified-filter-api'></a>
```cs
public class Accommodation
{
public Guid Id { get; set; }
public Guid? LocationId { get; set; }
public string? City { get; set; }
public int Capacity { get; set; }
}
```
<sup><a href='/src/Snippets/GlobalFilterSnippets.cs#L308-L318' title='Snippet source file'>snippet source</a> | <a href='#snippet-simplified-filter-api' title='Start of snippet'>anchor</a></sup>
<a id='snippet-simplified-filter-api-1'></a>
```cs
var filters = new Filters<MyDbContext>();

// VALID: Simplified API with primary key access
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.Id != Guid.Empty);

// VALID: Simplified API with foreign key access
var allowedLocationId = Guid.NewGuid();
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.LocationId == allowedLocationId);

// VALID: Simplified API with nullable foreign key check
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.LocationId != null);

// INVALID: Simplified API accessing scalar property (will cause runtime error!)
// filters.Add<Accommodation>(
// filter: (_, _, _, a) => a.City == "London"); // ERROR: City is not a key

// INVALID: Simplified API accessing scalar property (will cause runtime error!)
// filters.Add<Accommodation>(
// filter: (_, _, _, a) => a.Capacity > 10); // ERROR: Capacity is not a key

// For non-key properties, use the full API with projection:
filters.For<Accommodation>().Add(
projection: a => a.City,
filter: (_, _, _, city) => city == "London");

filters.For<Accommodation>().Add(
projection: a => new { a.City, a.Capacity },
filter: (_, _, _, x) => x.City == "London" && x.Capacity > 10);

// COMPARISON: These are equivalent when filter only accesses keys
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.Id != Guid.Empty);
// Equivalent to:
filters.For<Accommodation>().Add(
projection: _ => _, // Identity projection
filter: (_, _, _, a) => a.Id != Guid.Empty);

EfGraphQLConventions.RegisterInContainer<MyDbContext>(
services,
resolveFilters: _ => filters);
```
<sup><a href='/src/Snippets/GlobalFilterSnippets.cs#L322-L368' title='Snippet source file'>snippet source</a> | <a href='#snippet-simplified-filter-api-1' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### When to Use the Simplified API

Use `filters.Add<TEntity>(filter: ...)` when the filter **only** accesses:

* **Primary keys**: `Id`, `EntityId`, `CompanyId` (matching the entity type name)
* **Foreign keys**: Properties ending with `Id` like `ParentId`, `CategoryId`, `LocationId`

The simplified API uses identity projection (`_ => _`) internally, which in EF projections only guarantees that key properties are loaded.

### Key Property Detection Rules

A property is considered a **primary key** if it is:

* Named `Id`
* Named `{TypeName}Id` (e.g., `CompanyId` for `Company` entity)
* Named `{TypeName}Id` where TypeName has suffix removed: `Entity`, `Model`, `Dto`
* Example: `CompanyId` in `CompanyEntity` class

A property is considered a **foreign key** if:

* Name ends with `Id` (but is not solely `Id`)
* Not identified as a primary key
* Type is `int`, `long`, `short`, or `Guid` (nullable or non-nullable)

### Restrictions

**IMPORTANT**: Do not access scalar properties (like `Name`, `City`, `Capacity`) or navigation properties (like `Parent`, `Category`) with the simplified API. These properties are not loaded by identity projection and will cause runtime errors.

For non-key properties, use the full API with explicit projection:

```csharp
// INVALID - Will cause runtime error
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.City == "London"); // City is NOT a key

// VALID - Explicit projection for scalar properties
filters.For<Accommodation>().Add(
projection: a => a.City,
filter: (_, _, _, city) => city == "London");
```

### Comparison with Full API

The simplified API is syntactic sugar for the identity projection pattern:

```csharp
// Simplified API
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.Id != Guid.Empty);

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

### Analyzer Support

Three analyzer diagnostics help ensure correct usage:

* **GQLEF004** (Info): Suggests using the simplified API when identity projection only accesses keys
* **GQLEF005** (Error): Prevents accessing non-key properties with simplified API
* **GQLEF006** (Error): Prevents accessing non-key properties with identity projection

### Migration Guide

Existing code using identity projection with filters that only access keys can be migrated to the simplified API:

**Before:**
```csharp
filters.For<Product>().Add(
projection: _ => _,
filter: (_, _, _, p) => p.CategoryId == allowedCategoryId);
```

**After:**
```csharp
filters.Add<Product>(
filter: (_, _, _, p) => p.CategoryId == allowedCategoryId);
```

The simplified API makes intent clearer and reduces boilerplate while maintaining the same runtime behavior
90 changes: 90 additions & 0 deletions docs/mdsource/filters.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,93 @@ This is useful when:
* User permissions are stored in the database
* Filter logic requires async operations
* Complex checks involve multiple data sources


## Simplified Filter API

For filters that only need to access **primary key** or **foreign key** properties, a simplified API is available that eliminates the need to specify a projection:

snippet: simplified-filter-api

### When to Use the Simplified API

Use `filters.Add<TEntity>(filter: ...)` when the filter **only** accesses:

* **Primary keys**: `Id`, `EntityId`, `CompanyId` (matching the entity type name)
* **Foreign keys**: Properties ending with `Id` like `ParentId`, `CategoryId`, `LocationId`

The simplified API uses identity projection (`_ => _`) internally, which in EF projections only guarantees that key properties are loaded.

### Key Property Detection Rules

A property is considered a **primary key** if it is:

* Named `Id`
* Named `{TypeName}Id` (e.g., `CompanyId` for `Company` entity)
* Named `{TypeName}Id` where TypeName has suffix removed: `Entity`, `Model`, `Dto`
* Example: `CompanyId` in `CompanyEntity` class

A property is considered a **foreign key** if:

* Name ends with `Id` (but is not solely `Id`)
* Not identified as a primary key
* Type is `int`, `long`, `short`, or `Guid` (nullable or non-nullable)

### Restrictions

**IMPORTANT**: Do not access scalar properties (like `Name`, `City`, `Capacity`) or navigation properties (like `Parent`, `Category`) with the simplified API. These properties are not loaded by identity projection and will cause runtime errors.

For non-key properties, use the full API with explicit projection:

```csharp
// INVALID - Will cause runtime error
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.City == "London"); // City is NOT a key

// VALID - Explicit projection for scalar properties
filters.For<Accommodation>().Add(
projection: a => a.City,
filter: (_, _, _, city) => city == "London");
```

### Comparison with Full API

The simplified API is syntactic sugar for the identity projection pattern:

```csharp
// Simplified API
filters.Add<Accommodation>(
filter: (_, _, _, a) => a.Id != Guid.Empty);

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

### Analyzer Support

Three analyzer diagnostics help ensure correct usage:

* **GQLEF004** (Info): Suggests using the simplified API when identity projection only accesses keys
* **GQLEF005** (Error): Prevents accessing non-key properties with simplified API
* **GQLEF006** (Error): Prevents accessing non-key properties with identity projection

### Migration Guide

Existing code using identity projection with filters that only access keys can be migrated to the simplified API:

**Before:**
```csharp
filters.For<Product>().Add(
projection: _ => _,
filter: (_, _, _, p) => p.CategoryId == allowedCategoryId);
```

**After:**
```csharp
filters.Add<Product>(
filter: (_, _, _, p) => p.CategoryId == allowedCategoryId);
```

The simplified API makes intent clearer and reduces boilerplate while maintaining the same runtime behavior
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.0.1</Version>
<Version>34.1.0</Version>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>EntityFrameworkCore, EntityFramework, GraphQL</PackageTags>
Expand Down
Loading