Skip to content

GraphQL for .NET Teams on SQL Server

.NET teams running SQL Server face a common problem with GraphQL tooling: most frameworks assume PostgreSQL, and adding GraphQL typically means migrating your database or running a foreign runtime.

FraiseQL is different. It generates T-SQL natively, deploys as a single binary, and integrates with the SQL Server infrastructure your team already uses — Windows Authentication, Azure AD / Entra ID, stored procedures, indexed views, and Row-Level Security.

BeforeAfter
DatabaseSQL ServerSQL Server (unchanged)
AuthWindows Auth / Azure ADWindows Auth / Azure AD (unchanged)
Stored proceduresUsed by applicationCan back FraiseQL mutations
ViewsReporting, EF projectionsFraiseQL reads from SQL views
Entity FrameworkORM for writesContinues to handle writes
DapperAd-hoc queriesContinues to run alongside
GraphQL APINoneFraiseQL serves it

Your SQL Server schema does not change. Your EF migrations still run. FraiseQL adds a read-optimized GraphQL layer over SQL views you create — it doesn’t replace your existing data access patterns.

A common pattern is to keep Entity Framework for writes (mutations go through your domain logic) and use FraiseQL for complex reads (GraphQL queries that would otherwise require custom controller endpoints):

Client
├─── GraphQL query ──► FraiseQL ──► SQL Server view (v_order, v_customer, ...)
└─── REST / mutation ──► .NET API ──► Entity Framework ──► SQL Server tables

FraiseQL and Entity Framework connect to the same SQL Server database. They operate independently — FraiseQL reads, EF writes. You can adopt FraiseQL incrementally, starting with one or two entities.

Assume you have an existing Orders table managed by Entity Framework:

// Existing EF model
public class Order
{
public Guid Id { get; set; }
public string Status { get; set; }
public decimal Total { get; set; }
public Guid CustomerId { get; set; }
public DateTime CreatedAt { get; set; }
}

To expose orders via GraphQL:

Step 1: Create a SQL view (FraiseQL reads from views, not tables directly)

CREATE VIEW v_order AS
SELECT
o.id,
JSON_QUERY(
(SELECT
CAST(o.id AS NVARCHAR(36)) AS id,
o.status AS status,
CAST(o.total AS FLOAT) AS total,
CAST(o.customer_id AS NVARCHAR(36)) AS customerId,
FORMAT(o.created_at, 'yyyy-MM-ddTHH:mm:ssZ') AS createdAt
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER)
) AS data
FROM orders o;

Step 2: Define the FraiseQL type

# schema.py (FraiseQL schema is defined in Python, TypeScript, or Go)
import fraiseql
@fraiseql.type
class Order:
id: str
status: str
total: float
customer_id: str
created_at: str
@fraiseql.query
def orders(
limit: int = 20,
offset: int = 0,
status: str | None = None,
) -> list[Order]:
return fraiseql.config(sql_source="v_order")

Step 3: Configure and run

fraiseql.toml
[project]
name = "myapp-graphql"
[database]
url = "${DATABASE_URL}"
[server]
port = 8080
8080/graphql
fraiseql run

Result: Your existing SQL Server data is now queryable via GraphQL:

query {
orders(status: "pending", limit: 10) {
id
status
total
createdAt
}
}

No changes to your EF migrations, domain model, or write path.

fraiseql.toml
[database]
url = "server=SQLSERVER01;database=myapp;integratedSecurity=true;trustServerCertificate=true"

Run FraiseQL under a domain service account (DOMAIN\fraiseql-svc). The service account needs db_datareader on the database.

Your team likely has stored procedures for write operations. FraiseQL mutations can call them directly:

-- Existing stored procedure
CREATE PROCEDURE sp_update_order_status
@order_id UNIQUEIDENTIFIER,
@status NVARCHAR(50)
AS
BEGIN
UPDATE orders SET status = @status WHERE id = @order_id;
SELECT CAST(id AS NVARCHAR(36)) AS id, status FROM orders WHERE id = @order_id;
END
schema.py
@fraiseql.type
class UpdateOrderResult:
id: str
status: str
@fraiseql.mutation
def update_order_status(order_id: str, status: str) -> UpdateOrderResult:
return fraiseql.config(fn_source="sp_update_order_status")
mutation {
updateOrderStatus(orderId: "...", status: "shipped") {
id
status
}
}

No T-SQL rewriting. Your existing stored procedures work as-is.

docker-compose.yml
services:
fraiseql:
image: ghcr.io/fraiseql/fraiseql:latest
ports:
- "8080:8080"
environment:
DATABASE_URL: "server=SQLSERVER01;database=myapp;integratedSecurity=true;trustServerCertificate=true"
volumes:
- ./fraiseql.toml:/app/fraiseql.toml:ro
- ./schema.py:/app/schema.py:ro

For Windows Authentication in Docker, the container must run with the domain service account’s credentials and the host must be domain-joined.

It can be useful to see exactly what T-SQL FraiseQL executes. Enable query logging to capture generated statements:

[server]
log_queries = true

A GraphQL query like:

query {
orders(status: "pending", limit: 5) {
id
status
total
}
}

Generates approximately:

SELECT TOP 5 data
FROM v_order
WHERE JSON_VALUE(data, '$.status') = 'pending'
ORDER BY (SELECT NULL);

No ORM magic. Plain T-SQL. You can run it in SSMS, add it to Query Store analysis, or add a computed column index on $.status to speed it up.

  1. Install FraiseQL

    Terminal window
    curl -fsSL https://install.fraiseql.dev | sh
    fraiseql --version
  2. Create your first view over an existing table

  3. Write a schema.py with your types and queries

  4. Run

    Terminal window
    fraiseql run
  5. Open GraphiQL at http://localhost:8080/graphql and explore your data

SQL Server Enterprise Guide

Azure AD, Always On, TDE, and compliance patterns for regulated industries. Enterprise Guide

Azure Deployment

End-to-end Azure SQL + Container Apps deployment with Managed Identity. Azure Guide

SQL Server Setup

Connection strings, schema design, and SQL Server-specific patterns. SQL Server Guide

Quick Start

General getting-started guide with installation and first API walkthrough. Quick Start