Skip to content

Introduces MakeFieldsNullableTypeInterceptor to prevent null propagation#175

Open
MK-simplic wants to merge 1 commit into
masterfrom
e_nullable-type-interceptor
Open

Introduces MakeFieldsNullableTypeInterceptor to prevent null propagation#175
MK-simplic wants to merge 1 commit into
masterfrom
e_nullable-type-interceptor

Conversation

@MK-simplic
Copy link
Copy Markdown

@MK-simplic MK-simplic commented Apr 22, 2026

Summary by CodeRabbit

  • New Features
    • Added optional configuration parameter to GraphQL setup, allowing fields to be nullable for missing values (enabled by default). This provides more flexibility in schema generation and error handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Walkthrough

The pull request extends the UseGraphQL<TQuery> extension method with an optional parameter that conditionally registers a HotChocolate type interceptor. This interceptor modifies GraphQL schema fields to make their outer types nullable during schema completion.

Changes

Cohort / File(s) Summary
GraphQL Configuration Extension
src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs
Updated UseGraphQL<TQuery> method signature to include optional tolerateMissingFieldValues parameter (defaults to true). Conditionally registers MakeFieldsNullableTypeInterceptor via TryAddTypeInterceptor when flag is enabled. Updated imports and documentation.
GraphQL Schema Type Interceptor
src/Simplic.OxS.Server/GraphQL/MakeFieldsNullableTypeInterceptor.cs
New internal TypeInterceptor implementation that runs during type completion. Processes non-introspection ObjectType configurations and modifies field types to have nullable outer types via TypeInspector.ChangeNullability(). Filters introspection fields and HotChocolate namespace types.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Svc as IServiceCollection
    participant REB as IRequestExecutorBuilder
    participant HC as HotChocolate Type System
    participant Interc as MakeFieldsNullableTypeInterceptor

    App->>Svc: UseGraphQL<TQuery>(tolerateMissingFieldValues: true)
    Svc->>REB: builder configuration
    alt tolerateMissingFieldValues is true
        REB->>Interc: TryAddTypeInterceptor()
    end
    Svc->>HC: Build schema
    HC->>Interc: OnBeforeCompleteName()
    loop For each field in ObjectType
        Interc->>Interc: Check if field requires nullability
        Interc->>Interc: ChangeNullability to true
        Interc->>HC: Update field type
    end
    HC-->>App: Schema with nullable fields
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Hops of joy through schema land,
Fields now nullable, perfectly planned!
A flag, an interceptor, type's gentle reform,
GraphQL bends gracefully to a kinder form.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing a new type interceptor to handle nullability behavior in GraphQL field definitions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch e_nullable-type-interceptor

Comment @coderabbitai help to get the list of available commands and usage tips.

@MK-simplic
Copy link
Copy Markdown
Author

GraphQL Nullable Fields Interceptor

The Problem: Null Propagation

If a document in MongoDB is missing the InternalCode field, a standard GraphQL query would return null for the entire User object.

Per GraphQL spec, when a resolver returns null for a NonNull (!) field, the server must propagate the null up to the nearest nullable parent — or, if no nullable parent exists, fail the whole selection set. For legacy MongoDB documents that don't carry every field the current C# model declares, this turns benign data gaps into hard query failures.

The Solution: MakeFieldsNullableTypeInterceptor

This interceptor automatically rewrites the GraphQL schema during the initialization phase. It identifies user-defined object types and forces their output fields to be nullable, regardless of the C# type definition.

How it works

  1. Filters types — ignores internal HotChocolate types (like __Schema, PageInfo, or Connection) to ensure framework features like paging still work correctly.
  2. Inspects fields — loops through every field in user-defined object types.
  3. Changes nullability — uses the TypeInspector to flip the nullability flag to true.

Comparison: Before vs. After

1. C# Source Code

public class Product
{
    public string Sku { get; set; }
    public decimal Price { get; set; }
}

2. Generated GraphQL SDL

Without Interceptor (Strict) With Interceptor (Tolerant)
type Product { type Product {
sku: String! sku: String
price: Decimal! price: Decimal
} }

3. JSON Response Example

Given a document where sku is missing:

Without interceptor (strict, spec-conformant):

{
  "data": { "product": null },
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Product.sku.",
      "path": ["product", "sku"]
    }
  ]
}

With interceptor (tolerant):

{
  "data": {
    "product": {
      "sku": null,
      "price": 12.50
    }
  }
}

Benefits

  • Data resilience — prevents one missing field from breaking the entire UI component.
  • Legacy compatibility — seamlessly handles MongoDB documents that were saved before certain fields were added to the C# models.
  • Developer velocity — no need to manually mark every C# property as nullable just to satisfy the GraphQL schema.

Integration

The interceptor is registered by default through the shared Simplic.OxS.Server extension:

services.UseGraphQL<Query>();                             // tolerant (default)
services.UseGraphQL<Query>(tolerateMissingFieldValues: false); // strict, spec-conformant

When enabled, MakeFieldsNullableTypeInterceptor is registered via TryAddTypeInterceptor, so every service that consumes Simplic.OxS.Server benefits automatically — no per-service startup change needed.

Caveats

  • Schema becomes broadly nullable. Frontend/codegen clients must expect fields that were previously String! to now be String. Strongly typed codegens (gql-codegen, Strawberry Shake) will regenerate — calling code may need null-checks it didn't need before.
  • Framework fields are preserved. The interceptor skips types whose namespace starts with HotChocolate, so paging (Connection, Edge, PageInfo) and introspection remain strict.
  • Opt-out per service. Pass tolerateMissingFieldValues: false to UseGraphQL<TQuery> if a specific service needs strict non-null guarantees.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs (1)

25-37: ⚠️ Potential issue | 🟠 Major

Defaulting tolerance to true introduces a silent schema-contract shift.

Line 25 makes existing UseGraphQL<TQuery>() call sites automatically emit looser nullability, and Line 36 applies it globally. This is high-impact for schema consumers/tooling (including exported schema snapshots). Consider defaulting to false and requiring explicit opt-in.

Suggested fix
-            bool tolerateMissingFieldValues = true) where TQuery : class
+            bool tolerateMissingFieldValues = false) where TQuery : class
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs` around lines 25 - 37,
The method UseGraphQL<TQuery>() currently defaults tolerateMissingFieldValues to
true causing an implicit global nullability relaxation; change the parameter
default to false (tolerateMissingFieldValues = false) so callers must opt-in,
keep the existing conditional that adds MakeFieldsNullableTypeInterceptor only
when tolerateMissingFieldValues is true, and update any related
tests/docs/examples that assumed the previous default to explicitly pass true
where needed; reference the UseGraphQL<TQuery>() signature, the
tolerateMissingFieldValues parameter, and the MakeFieldsNullableTypeInterceptor
addition in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs`:
- Around line 24-25: The method parameter 'builder' in GraphQLExtension.cs is
being assigned null but is declared non-nullable; change its type to a nullable
delegate by updating the parameter declaration from
Action<IRequestExecutorBuilder> builder = null to
Action<IRequestExecutorBuilder>? builder = null (i.e., make the 'builder'
parameter nullable) so it aligns with nullable reference types and the method
signature (where TQuery : class).

In `@src/Simplic.OxS.Server/GraphQL/MakeFieldsNullableTypeInterceptor.cs`:
- Around line 35-37: The namespace check in MakeFieldsNullableTypeInterceptor is
too broad: the runtimeType?.Namespace StartsWith("HotChocolate", ...) will also
match names like "HotChocolateExtensions" and skip user types; change the guard
to only skip the exact HotChocolate root or its subnamespaces by checking either
Namespace.Equals("HotChocolate", StringComparison.Ordinal) ||
Namespace.StartsWith("HotChocolate.", StringComparison.Ordinal) (or the
equivalent) so runtimeType.Namespace only bypasses the interceptor for
"HotChocolate" and "HotChocolate.*".

---

Outside diff comments:
In `@src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs`:
- Around line 25-37: The method UseGraphQL<TQuery>() currently defaults
tolerateMissingFieldValues to true causing an implicit global nullability
relaxation; change the parameter default to false (tolerateMissingFieldValues =
false) so callers must opt-in, keep the existing conditional that adds
MakeFieldsNullableTypeInterceptor only when tolerateMissingFieldValues is true,
and update any related tests/docs/examples that assumed the previous default to
explicitly pass true where needed; reference the UseGraphQL<TQuery>() signature,
the tolerateMissingFieldValues parameter, and the
MakeFieldsNullableTypeInterceptor addition in your changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ab3a0e2-9c52-45b3-b4f0-daf3c334ce43

📥 Commits

Reviewing files that changed from the base of the PR and between 381beae and 6d86da0.

📒 Files selected for processing (2)
  • src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs
  • src/Simplic.OxS.Server/GraphQL/MakeFieldsNullableTypeInterceptor.cs

Comment on lines +24 to +25
Action<IRequestExecutorBuilder> builder = null,
bool tolerateMissingFieldValues = true) where TQuery : class
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify nullable context + detect similar non-nullable delegate defaults to null.
fd -e csproj --exec rg -n "<Nullable>.*</Nullable>" {}
rg -nP --type=cs '\bAction<[^>]+>\s+\w+\s*=\s*null\b'

Repository: simplic/simplic-oxs

Length of output: 1195


🏁 Script executed:

cat -n src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs | head -50

Repository: simplic/simplic-oxs

Length of output: 2607


Mark the optional builder callback as nullable.

Line 24 assigns null to a non-nullable delegate type. Use Action<IRequestExecutorBuilder>? to align with the enabled nullable reference type context.

Suggested fix
-            Action<IRequestExecutorBuilder> builder = null,
+            Action<IRequestExecutorBuilder>? builder = null,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Action<IRequestExecutorBuilder> builder = null,
bool tolerateMissingFieldValues = true) where TQuery : class
Action<IRequestExecutorBuilder>? builder = null,
bool tolerateMissingFieldValues = true) where TQuery : class
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Simplic.OxS.Server/Extensions/GraphQLExtension.cs` around lines 24 - 25,
The method parameter 'builder' in GraphQLExtension.cs is being assigned null but
is declared non-nullable; change its type to a nullable delegate by updating the
parameter declaration from Action<IRequestExecutorBuilder> builder = null to
Action<IRequestExecutorBuilder>? builder = null (i.e., make the 'builder'
parameter nullable) so it aligns with nullable reference types and the method
signature (where TQuery : class).

Comment on lines +35 to +37
if (runtimeType?.Namespace is { } ns &&
ns.StartsWith("HotChocolate", System.StringComparison.Ordinal))
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tighten the namespace guard to avoid skipping user types.

Line 36 uses StartsWith("HotChocolate", ...), which also matches namespaces like HotChocolateExtensions and can unintentionally bypass this interceptor for user-defined runtime types.

Suggested fix
-            if (runtimeType?.Namespace is { } ns &&
-                ns.StartsWith("HotChocolate", System.StringComparison.Ordinal))
+            if (runtimeType?.Namespace is { } ns &&
+                (ns.Equals("HotChocolate", System.StringComparison.Ordinal) ||
+                 ns.StartsWith("HotChocolate.", System.StringComparison.Ordinal)))
             {
                 return;
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (runtimeType?.Namespace is { } ns &&
ns.StartsWith("HotChocolate", System.StringComparison.Ordinal))
{
if (runtimeType?.Namespace is { } ns &&
(ns.Equals("HotChocolate", System.StringComparison.Ordinal) ||
ns.StartsWith("HotChocolate.", System.StringComparison.Ordinal)))
{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Simplic.OxS.Server/GraphQL/MakeFieldsNullableTypeInterceptor.cs` around
lines 35 - 37, The namespace check in MakeFieldsNullableTypeInterceptor is too
broad: the runtimeType?.Namespace StartsWith("HotChocolate", ...) will also
match names like "HotChocolateExtensions" and skip user types; change the guard
to only skip the exact HotChocolate root or its subnamespaces by checking either
Namespace.Equals("HotChocolate", StringComparison.Ordinal) ||
Namespace.StartsWith("HotChocolate.", StringComparison.Ordinal) (or the
equivalent) so runtimeType.Namespace only bypasses the interceptor for
"HotChocolate" and "HotChocolate.*".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant