- F# 10, C# 14, .NET 10, nullability checks enabled
- .NET SDK roll-forward policy in #file:'global.json'; the exact SDK version is pinned in the GitHub workflows and #file:'build/Program.fs'
- Common parameters specified in #file:'Directory.Build.props'
- Central NuGet package version management – versions go in #file:'Packages.props', not in
.fsprojfiles - Build:
dotnet build FSharp.Data.GraphQL.slnx - Integration tests and their server live in
FSharp.Data.GraphQL.Integration.slnx - Full build pipeline (FAKE):
build.cmd/build.sh, which rundotnet run --project build\Build.fsproj
/
├── src/
│ ├── FSharp.Data.GraphQL.Shared/ – AST, parser, type system, validation, introspection
│ ├── FSharp.Data.GraphQL.Server/ – schema definition and execution engine
│ ├── FSharp.Data.GraphQL.Server.Middleware/ – built-in middlewares (ObjectListFilter, query weight, …)
│ ├── FSharp.Data.GraphQL.Server.Relay/ – Relay connections and node support
│ ├── FSharp.Data.GraphQL.Server.AspNetCore/ – ASP.NET Core integration (HTTP and WebSocket)
│ ├── FSharp.Data.GraphQL.Server.Giraffe/ – Giraffe integration
│ ├── FSharp.Data.GraphQL.Server.Oxpecker/ – Oxpecker integration
│ ├── FSharp.Data.GraphQL.Client/ – client type provider runtime
│ └── FSharp.Data.GraphQL.Client.DesignTime/ – client type provider design-time component
├── tests/
│ ├── FSharp.Data.GraphQL.Tests/ – xUnit unit tests
│ ├── FSharp.Data.GraphQL.Tests.Sql/ – LINQ to SQL tests
│ ├── FSharp.Data.GraphQL.IntegrationTests/ – xUnit integration tests (in-process hosts via WebApplicationFactory)
│ ├── FSharp.Data.GraphQL.IntegrationTests.Server/
│ └── FSharp.Data.GraphQL.Benchmarks/ – BenchmarkDotNet benchmarks
├── samples/ – sample servers and client provider scripts
├── build/ – FAKE build scripts
└── docs/ – FSharp.Formatting documentation source
If you need any source code you can find it in the following repositories:
FParsecmasterbranchXParsecmainbranchFSharp.Control.ReactivemasterbranchFSharp.Data.TypeProvidersmainbranchFSharp.SystemTextJsonmasterbranchFsToolkit.ErrorHandlingmasterbranchGiraffemasterbranchOxpeckermainbranchxUnit– test framework
Use GitHub MCP tools for code search in these repositories when needed.
- Always prefer PowerShell for all shell operations.
- Always prefer installing PowerShell modules over installing CLI tools.
- First of all use GitHub MCP tools.
- If no tool is available, use the
PowerShellForGitHubPowerShell module for all GitHub operations. Never use GitHub CLI (gh) or any other CLI tool for GitHub.
Agents discover servers from #file:'.mcp.json'; these are only hints on when to prefer one.
- For F# work, prefer the
F#server (FsLangMCP) overrg/plain text search whenever the task depends on symbol meaning, compile context, cross-project usage, diagnostics, or safe refactoring preview. Its tool package is pinned in #file:'.config/dotnet-tools.json' – rundotnet tool restorebefore first use. - Use the
GitHubserver for code search in dependency repositories. In Claude Code it authenticates through.claude/scripts/github-mcp-headers.ps1, which reuses the GitHub token stored by Git Credential Manager, because the server does not support the OAuth dynamic client registration Claude Code needs. - Use the
Microsoft Docsserver for official Microsoft and .NET documentation. - When adding or changing a server, keep
servers(VS Code / GitHub Copilot) andmcpServers(Claude Code) in sync and sorted alphabetically by key.
- Always use the latest F# 10 features over old syntax.
- If you are running outside of an IDE, or the IDE does not provide F# semantic tools, use FsLangMCP as the primary tool for F# code navigation, symbol discovery, diagnostics, usage search and refactoring preview. Prefer its semantic tools over plain text search.
- The compiler generates
IsCaseNameinstance properties (for exampleIsOk,IsNone) for each DU case – use them when a single-case check is needed. - Code is formatted with Fantomas using the settings in #file:'.editorconfig'. After making code changes, run
dotnet fantomas <changed files>(or #file:'format-changed-files.ps1') before committing.
- Prefer
taskCE overasyncCE. - When a method must return non-generic
Task, annotate the return type explicitly:member _.MyMethod (...) : Task = task { ... }. Never cast through pipelines. taskCE can awaitValueTaskAPIs directly.- Curried functions take the
CancellationTokenas their first parameter, so that it can be partially applied. - Never hand-roll wrappers such as
ValueTask (task { ... }),.AsTask ()round-trips or manualunit -> Taskthunks. Use the matchingIcedTasksCE instead (valueTask,valueTaskUnit,taskUnit,coldTask,cancellableTask,cancellableValueTask,backgroundTask, …) and always use the full CE names, not the short aliases (vTask,pvTask, …). This library does not referenceIcedTasksyet – adding it introduces a transitive dependency for every consumer, so confirm with the maintainers first.
- Prefer
voption(ValueSome/ValueNone) overoption. Fields, members, parameters and values shared between threads included: none of them is a reason to pickoption. Exception: when an API hands you'T optionand has novoptioncounterpart, unwrap it withOption.defaultValue/Option.defaultWithdirectly – do not insertValueOption.ofOptionjust to switch modules. - The mirror case, an API that takes
'T option(an optional argument?name = …, a field typed'T option): stay inValueOptionthrough the whole chain and convert once, last –x |> ValueOption.bind _.Value |> ValueOption.toOption, neverx |> ValueOption.toOption |> Option.bind _.Value. - Prefer
struct ('T1 * 'T2)over reference tuples, and anonymous struct records (struct {| ... |}) over tuples for return types of public functions and methods. - Never group with
Seq.groupBy– useToLookupfromSystem.Linq. It groups once into anILookup<'Key, 'T>instead of re-grouping on every enumeration and does not allocate a tuple per group. Pass a lambda (xs.ToLookup (fun x -> keyOf x)), not a bare function value. - When casting sequence items use
Seq.cast<TargetType>instead ofSeq.map (fun item -> item :> TargetType). - When concatenating two sequences or lists, prefer
seq { yield! xs; yield! ys }(or[ yield! xs; yield! ys ]for lists) over the@operator orSeq.append. - When pipe operators are used on a materializable collection multiple times in a row, prefer
Seqmodule for the chain and materialize at the end.
- Prefer underscore lambda syntax like
Seq.map _.NameoverSeq.map (fun x -> x.Name), but only when the expression is a simple member access. Complex expressions likeSeq.where (fun x -> x.Name = name)orSeq.map (fun x -> x.Field1, x.Field2)cannot be simplified. A member chain ending in a method call is not complex:_.Changed.Subscribe(handler). The shorthand needs its input type known, so pipe the value in first –value |> ValueOption.map _.Id, notValueOption.map _.Id value(FS0072). Never write a space in_.MethodCall()– it breaks parsing. - Simplify
Seq.map (fun x -> someFunction x)toSeq.map someFunction. - Prefer interpolated strings over
printffunctions for string formatting. Format specifiers like$"%s{value}"are valid in interpolated strings and help type inference. - Pass an explicit
StringComparisonto everyEquals,StartsWith,EndsWith,IndexOf,ContainsandCompare, and an explicit comparer to everyHashSet<string>andDictionary<string, _>.Ordinalby default; culture-sensitive comparison is a decision, never a default. UseOrdinalIgnoreCaseonly where the thing compared really is case-insensitive – never for GraphQL names (types, fields, arguments, directives, enum values), which are case-sensitive. - A slice of a string that is only inspected – compared, trimmed, scanned, matched against a prefix – is a
ReadOnlySpan<char>(text.AsSpan (start, length)), not aSubstring: the substring allocates a copy per call, the span does not. Materialize withSubstring/ToString ()only for the value that leaves the function or is stored. - The
StringComparisonrule applies to spans unchanged:MemoryExtensionshasStringComparisonoverloads ofStartsWith,EndsWith,Equals,CompareTo,IndexOfandContainsforReadOnlySpan<char>–s.AsSpan().TrimStart(' ').StartsWith("//", StringComparison.Ordinal). The overloads without one are the generic element-wiseReadOnlySpan<'T>ones – ordinal forcharby accident, not by statement. - A slice that must outlive the stack frame – kept in a record, captured by a closure (including a
taskCE), returned from a member – isReadOnlyMemory<char>(text.AsMemory (...)), never aReadOnlySpan<char>: a byref-like value cannot be stored. - To compare two slices without building either, use
String.CompareOrdinal (a, aIndex, b, bIndex, length)orspanA.Equals (spanB, StringComparison.Ordinal). - Use descriptive function names that indicate transformation direction.
-
Declare variables non-nullable; check for
nullat entry points only. -
Trust the SDK null annotations – do not add null checks when the type system says a value cannot be null.
-
Use
withNullfor null checks instead of boxing delegates/functions (avoidisNull (box value)). -
Prefer
matchonnulloverif isNull– it narrows the type and suppresses nullness warnings:// Preferred match someObject with | null -> () | someObject -> someObject.SomeProperty
-
Before suppressing a nullness warning (3261, 3262, …), exhaust these alternatives in order:
nonNull value– asserts non-null at runtime and fails fast.Unchecked.nonNull value– skips the runtime check; only when non-null was already verified upstream.- An inline
#nowarn/#warnonpair around the smallest possible scope – last resort for interop boundaries, centralised in a single helper rather than scattered across call sites.
-
Never suppress warnings file-wide;
#nowarnand#warnonare valid anywhere in a file.
-
A doc comment is either plain text with no tags at all, or fully explicit XML starting with
<summary>– never a mixture. The compiler adds<summary>by itself only when the comment has no tags, and silently escapes tags otherwise:- No tags anywhere → bare
///lines, no<summary>. - Any tag at all (
<see/>,<c/>,<para>,<param>,<returns>, …) → the comment must start with an explicit<summary>, and every<para>must be inside it.
// ✅ plain text — the compiler supplies <summary> /// Represents the result of a GraphQL execution. // ❌ a sibling tag without <summary> — the <param> is escaped into the summary and lost /// Executes a query. /// <param name="query">Query text</param> // ✅ any tag present, so the comment is explicit XML throughout /// <summary>Executes a query.</summary> /// <param name="query">Query text</param>
- No tags anywhere → bare
-
Every public API must have XML documentation.
-
The primary constructor of an F# class is the parenthesized parameter list that follows the type name, and it is documented as a member of its own: the type's
<summary>stays abovetype, and the constructor's<param>lines go on their own lines between the type name and the opening parenthesis, indented like the parameter list. Never put the constructor's<param>lines into the type's comment:/// <summary> /// Translates the events of one subscription into payloads. /// </summary> type internal SubscriptionPayloads /// <param name="logger">The logger of the connection.</param> /// <param name="options">The options of the middleware.</param> (logger : ILogger, options : Options) =
-
On an explicit interface implementation (
interface X with member _.M (...) = ...), write/// <inheritdoc />alone instead of restating the interface member's documentation, unless this implementation has behavior worth calling out beyond what the interface already documents – write a normal<summary>/<remarks>there instead. -
Refer to types and members through
<see cref="Type.Member"/>, never through<c>or plain text.<c>is for literal values only (JSON, GraphQL, setting names). Refer to language keywords through<see langword="null"/>. -
Split multi-paragraph documentation into
<para>elements inside<summary>– bare line breaks are collapsed by documentation renderers. -
Write enumerations as
<list type="bullet">(ortype="number") with<item><description>…</description></item>, never as Markdown-style bullets.
Sort open statements alphabetically within groups: System first, Microsoft second, FSharp third; then other external namespaces; then this solution's namespaces. open type goes last in each group. Type and module aliases form a separate final group.
This is how to define a non-default F# class constructor:
type DerivedClass =
inherit BaseClass
new (``arguments here``) as ``created object``
=
// create any objects used in the base class constructor
let fieldValue = ""
{
inherit
BaseClass (``arguments here``)
}
then
``created object``.otherField <- fieldValue
[<DefaultValue>]
val mutable otherField : FieldTypeAlways prefer F# class initializers over property assignment! You absolutely must use F# class initializers instead of property assignment!
Class declaration:
type MyClass (someConstructorParam : string) =
member ReadOnlyProperty = someConstructorParam
member val MutableProperty1 = "" with get, set
member val MutableProperty2 = "" with get, setWrong:
let myClass = MyClass("some value")
myClass.MutableProperty1 <- "new value"
myClass.MutableProperty2 <- "new value"Right:
let myClass =
MyClass(
// constructor parameters go first without names
"some value",
// then mutable properties go next with names
MutableProperty1 = "new value",
MutableProperty2 =
// operations must be placed into parentheses
(5 |> string)
)// AutoOpen makes the module automatically available without an explicit open statement
// Extension makes the members visible to C#
[<AutoOpen; Extension>]
module MyTypeExtensions =
type MyType with
// Extension is visible to C#
// CompiledName makes the method name friendly to C#
[<Extension; CompiledName "ExtensionMethod">]
member this.ExtensionMethod (param1 : string) : ReturnType =
()- Use PascalCase for modules, types, and public members.
- Use camelCase for
letbindings, functions, private fields, and local variables. - Prefix interface names with
I(e.g.,ISchema). - Do not prefix type parameters with
Tin new code (e.g., use'Resultinstead of'TResult). - Name tests using spaces (e.g.,
letTest name with spaces() = ...).
IMPORTANT!!! When you create a code file, ALWAYS put it at the end of fsproj project in order to avoid missing types that must come above.
During the implementation, if you need some types or members defined in the other project but that project is not referenced, then:
- stop implementing the solution;
- respond with summary about what is needed and which project it is defined in;
- ask if you can add the reference before continuing coding.
- Tests use xUnit.
- If you work with tests, then do not build the whole solution as it is large and the build happens very slow. Run the tests individually or the whole test project instead.
- Prefer running tests through the IDE's MCP test tools; fall back to the
--no-buildswitch ofdotnet testfirst to speed up execution when those tools are unavailable or fail to run the tests, and use the trx format for results so failures can be consumed and fixed. - Use
Assert.Equal,Assert.Collection,Assert.Contains/Assert.DoesNotContain,Assert.Empty/Assert.NotEmptyandAssert.Singlefor collection assertions – they work directly with F# lists, arrays and sequences. - Every assertion should produce a self-explanatory failure output.
- Async tests must return
Task, notAsyncorTask<unit>– always declare) : Task = task {.
We use dotnet test CLI to test the project.
- If the build fails with errors or non-zero exit code, fix it based on the error messages given and repeat the build step.
- If build or tests step fails, fix the errors and repeat from build. After that, report all relevant build errors, error messages and specific details about failing tests and their failure details.
- If any of the tests fail, check if the test, the test expectation (either inline in the test or a reference file configured for the test) or the implementation needs updating, and fix it.
- Builds without errors.
- Runs tests without errors. If some tests needed adjustments, those test expectations/baseline adjustments were done.
- If the acceptance criteria was not met, collect the error messages (build failures or failing tests) and report them.
- Make only high-confidence suggestions when reviewing code changes.
- Write code with good maintainability practices, including comments on why certain design decisions were made.
- Handle edge cases and write clear exception handling.
- Never duplicate code unless you got explicit confirmation that you are allowed to do so.
- All comments, documentation, README files, and markdown files must be written in English only.