Skip to content

C# SDK

The FraiseQL C# SDK is a schema authoring SDK: you define GraphQL types using .NET attributes and the FraiseQL compiler generates an optimized GraphQL API backed by your SQL views.

Terminal window
dotnet add package FraiseQL

Requirements: .NET 8+

FraiseQL C# uses .NET attributes for type definitions and a fluent builder API for queries and mutations:

ConstructGraphQL EquivalentPurpose
[GraphQLType]typeMark a class as a GraphQL output type
[GraphQLType(IsInput = true)]inputMark a class as a GraphQL input type
[GraphQLField]field metadataConfigure field types, nullability, scopes
SchemaRegistry.Query(name)...Query fieldWire a query to a SQL view
SchemaRegistry.Mutation(name)...Mutation fieldDefine a mutation

Annotate classes with [GraphQLType] and properties with [GraphQLField]. The Type parameter accepts GraphQL scalar names; when omitted the .NET type is auto-detected:

Schema/User.cs
using FraiseQL.Attributes;
[GraphQLType(SqlSource = "v_user")]
public class User
{
[GraphQLField(Type = "ID", Nullable = false)]
public string Id { get; set; } = "";
[GraphQLField(Nullable = false)]
public string Username { get; set; } = "";
[GraphQLField(Type = "Email", Nullable = false)]
public string Email { get; set; } = "";
[GraphQLField(Nullable = true)]
public string? Bio { get; set; }
[GraphQLField(Type = "DateTime", Nullable = false)]
public DateTimeOffset CreatedAt { get; set; }
}
Schema/Post.cs
using FraiseQL.Attributes;
[GraphQLType(SqlSource = "v_post")]
public class Post
{
[GraphQLField(Type = "ID", Nullable = false)]
public string Id { get; set; } = "";
[GraphQLField(Nullable = false)]
public string Title { get; set; } = "";
[GraphQLField(Type = "Slug", Nullable = false)]
public string Slug { get; set; } = "";
[GraphQLField(Nullable = false)]
public string Content { get; set; } = "";
[GraphQLField(Nullable = false)]
public bool IsPublished { get; set; }
[GraphQLField(Type = "DateTime", Nullable = false)]
public DateTimeOffset CreatedAt { get; set; }
}

Input types use [GraphQLType(IsInput = true)]:

Schema/CreatePostInput.cs
using FraiseQL.Attributes;
[GraphQLType(IsInput = true)]
public class CreatePostInput
{
[GraphQLField(Nullable = false)]
public string Title { get; set; } = "";
[GraphQLField(Nullable = false)]
public string Content { get; set; } = "";
[GraphQLField(Nullable = false)]
public bool IsPublished { get; set; }
}

Use SchemaRegistry.RegisterQuery in your application startup to wire queries to SQL views:

SchemaSetup.cs
using FraiseQL;
using FraiseQL.Registry;
public static class SchemaSetup
{
public static void Register()
{
// Register types first
SchemaRegistry.Register(typeof(User), typeof(Post), typeof(CreatePostInput));
// List query — maps to v_post view
SchemaRegistry.RegisterQuery("posts")
.SqlSource("v_post")
.ReturnType<Post>()
.ReturnsList(true)
.Arg("isPublished", "Boolean", nullable: true)
.Arg("limit", "Int", defaultValue: 20)
.Arg("offset", "Int", defaultValue: 0)
.Description("Fetch posts, optionally filtered by published status.")
.Register();
// Single-item query
SchemaRegistry.RegisterQuery("post")
.SqlSource("v_post")
.ReturnType<Post>()
.Nullable(true)
.Arg("id", "ID")
.Description("Fetch a single post by ID.")
.Register();
}
}

Query arguments matching columns in the backing view become SQL WHERE clauses automatically. See SQL Patterns → Automatic WHERE Clauses for details.


SchemaSetup.cs
// Maps to fn_create_post PostgreSQL function
SchemaRegistry.RegisterMutation("createPost")
.SqlSource("fn_create_post")
.ReturnType<Post>()
.Arg("input", "CreatePostInput")
.Operation("insert")
.InvalidatesViews(["v_post"])
.Description("Create a new blog post.")
.Register();
// Maps to fn_delete_post PostgreSQL function
SchemaRegistry.RegisterMutation("deletePost")
.SqlSource("fn_delete_post")
.ReturnType<Post>()
.Arg("id", "ID")
.RequiresRole("admin")
.Operation("delete")
.InvalidatesViews(["v_post"])
.Description("Soft-delete a post.")
.Register();

Mutations wire to PostgreSQL functions returning mutation_response. See SQL Patterns for complete function templates.


Use .RequiresRole(role) on query or mutation builders to restrict access:

SchemaRegistry.RegisterQuery("myPosts")
.SqlSource("v_post")
.ReturnType<Post>()
.ReturnsList(true)
.RequiresRole("member")
.InjectParams(new Dictionary<string, string> { ["authorId"] = "jwt:sub" })
.Register();

Field-level scope restrictions use [GraphQLField(Scope = "...")]:

[GraphQLField(Type = "Email", Nullable = false, Scope = "user:read:email")]
public string Email { get; set; } = "";

Transport annotations are optional. Omit them to serve an operation via GraphQL only. Extend [FraiseQLQuery] and [FraiseQLMutation] with RestPath and RestMethod named parameters. gRPC endpoints are auto-generated when [grpc] is enabled — no per-operation annotation needed. See gRPC Transport.

using FraiseQL.SDK;
// REST + GraphQL
[FraiseQLQuery(SqlSource = "v_post", RestPath = "/posts", RestMethod = "GET")]
public List<Post> Posts(int limit = 10) => null;
// With path parameter
[FraiseQLQuery(SqlSource = "v_post", RestPath = "/posts/{id}", RestMethod = "GET")]
public Post Post(Guid id) => null;
// REST mutation
[FraiseQLMutation(SqlSource = "create_post", Operation = "CREATE",
RestPath = "/posts", RestMethod = "POST")]
public Post CreatePost(string title, Guid authorId) => null;

Path parameters in RestPath (e.g., {id}) must match method parameter names exactly. A mismatch produces a compile-time error. Duplicate (method, path) pairs are also rejected at compile time.


  1. Call SchemaSetup.Register() in your export entry point:

    ExportSchema/Program.cs
    using FraiseQL.Registry;
    SchemaSetup.Register();
    await SchemaRegistry.ExportAsync("schema.json");
  2. Run the export tool:

    Terminal window
    dotnet run --project ExportSchema
  3. Compile with the FraiseQL CLI:

    Terminal window
    fraiseql compile --schema schema.json
  4. Serve the API:

    Terminal window
    fraiseql run

FraiseQLClient is a sealed, IDisposable async HTTP client for consuming FraiseQL (or any GraphQL) APIs from .NET applications:

using FraiseQL;
var options = new FraiseQLClientOptions
{
Endpoint = new Uri("https://api.example.com/graphql"),
Authorization = "Bearer your-token"
};
using var client = new FraiseQLClient(options);
// Query
var users = await client.QueryAsync<List<User>>("""
query { users { id username email } }
""");
// Mutation
var post = await client.MutateAsync<Post>("""
mutation($input: CreatePostInput!) {
createPost(input: $input) { id title }
}
""", new { input = new { title = "Hello", content = "World", isPublished = true } });
var options = new FraiseQLClientOptions
{
Endpoint = new Uri("https://api.example.com/graphql"),
Retry = new RetryOptions
{
MaxAttempts = 3,
BaseDelay = TimeSpan.FromMilliseconds(200),
MaxDelay = TimeSpan.FromSeconds(5),
Jitter = true
}
};
ExceptionTrigger
FraiseQLExceptionBase class for all SDK exceptions
GraphQLExceptionOne or more GraphQL errors in the response (exposes Errors list)
AuthenticationExceptionHTTP 401 or 403
RateLimitExceptionHTTP 429
FraiseQLTimeoutExceptionRequest exceeded timeout
NetworkExceptionConnection or DNS failure

For dynamic tokens (e.g. refresh flows), pass a factory instead of a static string:

var options = new FraiseQLClientOptions
{
Endpoint = new Uri("https://api.example.com/graphql"),
AuthorizationFactory = async () => $"Bearer {await GetFreshTokenAsync()}"
};