MyDevTime.SqlDataProvider.AspDotNetCore.gRPC 2.0.1

dotnet add package MyDevTime.SqlDataProvider.AspDotNetCore.gRPC --version 2.0.1
                    
NuGet\Install-Package MyDevTime.SqlDataProvider.AspDotNetCore.gRPC -Version 2.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MyDevTime.SqlDataProvider.AspDotNetCore.gRPC" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MyDevTime.SqlDataProvider.AspDotNetCore.gRPC" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="MyDevTime.SqlDataProvider.AspDotNetCore.gRPC" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MyDevTime.SqlDataProvider.AspDotNetCore.gRPC --version 2.0.1
                    
#r "nuget: MyDevTime.SqlDataProvider.AspDotNetCore.gRPC, 2.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MyDevTime.SqlDataProvider.AspDotNetCore.gRPC@2.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MyDevTime.SqlDataProvider.AspDotNetCore.gRPC&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=MyDevTime.SqlDataProvider.AspDotNetCore.gRPC&version=2.0.1
                    
Install as a Cake Tool

MyDevTime.SqlDataProvider

A config-driven SQL data provider framework that exposes arbitrary database queries through multiple ASP.NET Core protocols. Define SQL queries in any IConfiguration source (appsettings.json, environment variables, Azure Key Vault, etc.) and the framework automatically exposes them via GraphQL, OData, SignalR, gRPC, and MCP (Model Context Protocol) — with schema auto-discovery, typed endpoints, filtering, sorting, and projection.

Supports .NET 8, .NET 9, and .NET 10.

Table of Contents

Packages

Package Version Description
MyDevTime.SqlDataProvider NuGet Core library — connection factories, SQL dialects, query builder, schema discovery
MyDevTime.SqlDataProvider.AspDotNetCore.GraphQl NuGet HotChocolate GraphQL integration
MyDevTime.SqlDataProvider.AspDotNetCore.OData NuGet OData REST integration
MyDevTime.SqlDataProvider.AspDotNetCore.SignalR NuGet SignalR real-time integration
MyDevTime.SqlDataProvider.AspDotNetCore.gRPC NuGet gRPC integration
MyDevTime.SqlDataProvider.AspDotNetCore.Mcp NuGet MCP integration for AI assistants

Features

  • Config-driven — define endpoints via IConfiguration (appsettings.json, environment variables, or any configuration provider), no code changes needed
  • Schema auto-discovery — column names, types, and nullability discovered at startup via SchemaOnly execution
  • Multiple protocols — GraphQL (HotChocolate), OData, SignalR, gRPC, MCP
  • Multi-database — pluggable IDbConnectionFactory with built-in SQL Server support; add any ADO.NET provider (SQLite, PostgreSQL, MySQL, etc.)
  • SQL dialect abstractionISqlDialect handles paging syntax and column quoting per database (ANSI, SQL Server, PostgreSQL, MySQL built-in)
  • SQL injection preventionValidColumns whitelist enforced on all filter/sort/projection inputs
  • IQueryable provider — all protocols use SqlDataProviderQueryable<T> with runtime-generated CLR types, translating LINQ expression trees to parameterized SQL
  • Filtering — per-endpoint opt-in with column-level control; recursive AND/OR filter trees across all protocols
  • Sorting — single and multi-column, ASC/DESC
  • Projection — auto-projection from GraphQL selection set; explicit column selection for other protocols
  • Paging — offset-based with skip/take and totalCount across all protocols (default: 50 rows)
  • Hot-reload — OData, SignalR, gRPC, and MCP reload endpoint config on IConfiguration changes without restart (via ChangeToken.OnChange)
  • AI-ready — MCP protocol exposes endpoints as tools for Claude, VS Code, and other AI assistants

Quick Start

1. Install the packages

dotnet add package MyDevTime.SqlDataProvider
dotnet add package MyDevTime.SqlDataProvider.AspDotNetCore.GraphQl   # and/or other protocols

2. Register the provider

// GraphQL
builder.Services
    .AddGraphQLServer()
    .AddQueryType(d => d.Name("Query").Field("_empty").Resolve(""))
    .AddGraphQlSqlDataProvider();

// OData
builder.Services.AddControllers().AddOData(opt => opt.Filter().OrderBy().Count().SetMaxTop(1000));
builder.Services.AddODataSqlDataProvider();
// After builder.Build():
app.MapODataSqlDataProvider();  // enables $metadata and service document

// SignalR
builder.Services.AddSignalRSqlDataProvider();

// gRPC
builder.Services.AddGrpcSqlDataProvider();

// MCP (Model Context Protocol)
builder.Services.AddMcpServer()
    .AddMcpSqlDataProvider();

3. Register a database connection factory

using MyDevTime.SqlDataProvider.Connection;

// SQL Server (built-in)
builder.Services.AddDbConnectionFactory("mssql", new SqlClientConnectionFactory());

// Custom providers
builder.Services.AddDbConnectionFactory("sqlite", new SqliteConnectionFactory());
builder.Services.AddDbConnectionFactory("postgres", new NpgsqlConnectionFactory());

ProviderName in the profile config selects which factory to use. Duplicate registrations throw at startup.

4. Configure endpoints via IConfiguration

The framework binds the SqlRetrievalProfiles section from IConfiguration. This can come from any configuration provider — appsettings.json, environment variables, Azure Key Vault, etc. The required configuration path is SqlRetrievalProfiles at the root level.

appsettings.json example:

{
  "SqlRetrievalProfiles": [
    {
      "ProfileName": "AdventureWorks",
      "ConnectionString": "Server=...;Database=AdventureWorks;...",
      "ProviderName": "mssql",
      "RetrievalEndpointProfiles": [
        {
          "EndPointName": "Customers",
          "SqlQuery": "SELECT CustomerID, FirstName, LastName, EmailAddress FROM Customer",
          "AllowFiltering": true,
          "AllowSorting": true,
          "AllowProjection": true
        },
        {
          "EndPointName": "Products",
          "SqlQuery": "SELECT ProductID, Name, ListPrice FROM Product WHERE ListPrice > 0",
          "AllowFiltering": true,
          "AllowSorting": true,
          "AllowProjection": false
        }
      ]
    }
  ]
}

Column lists for filtering/sorting/projection are auto-discovered from the query at startup. You can also pin them explicitly:

{
  "EndPointName": "Customers",
  "SqlQuery": "SELECT CustomerID, FirstName, LastName FROM Customer",
  "AllowFiltering": true,
  "FilterableColumns": ["LastName"],
  "AllowSorting": true,
  "AllowProjection": true
}

Explicit lists are never overwritten by discovery.

Protocol Usage

GraphQL

Endpoints are exposed as top-level query fields named {ProfileName}_{EndPointName}. Results are wrapped in a paged response with items and totalCount.

# Basic query — auto-projects from selection set
{
  AdventureWorks_Customers {
    items { customerID firstName lastName }
    totalCount
  }
}

# Filtering (HotChocolate-style where input)
{
  AdventureWorks_Customers(where: { lastName: { eq: "Miller" } }) {
    items { customerID firstName lastName }
    totalCount
  }
}

# Multiple conditions (AND)
{
  AdventureWorks_Customers(
    where: { lastName: { eq: "Miller" }, firstName: { startsWith: "J" } }
  ) {
    items { customerID firstName lastName }
  }
}

# OR conditions
{
  AdventureWorks_Customers(
    where: { or: [{ lastName: { eq: "Miller" } }, { lastName: { eq: "Smith" } }] }
  ) {
    items { customerID firstName lastName }
  }
}

# HC-style sorting
{
  AdventureWorks_Customers(order: [{ lastName: ASC }, { firstName: DESC }]) {
    items { lastName firstName }
  }
}

# Paging
{
  AdventureWorks_Customers(skip: 20, take: 10) {
    items { customerID firstName lastName }
    totalCount
  }
}

Filter field names are camelCased. Supported operators by type:

Type Operators
String eq neq contains ncontains startsWith nstartsWith endsWith nendsWith in nin
Int / Long eq neq gt gte lt lte in nin
Float / Decimal eq neq gt gte lt lte in nin
Boolean eq neq
DateTime / Date eq neq gt gte lt lte in nin
UUID eq neq in nin

Note: GraphQL schema is built at startup. Adding or removing endpoints requires a restart.

OData

Uses Microsoft.AspNetCore.OData with [EnableQuery] — full OData query support via LINQ on our IQueryable provider.

GET /odata/$metadata                                          # EDM schema (CSDL XML)
GET /odata                                                    # Service document (entity set list)
GET /odata/AdventureWorks_Customers                           # All customers (default page size)
GET /odata/AdventureWorks_Customers?$filter=LastName eq 'Miller'
GET /odata/AdventureWorks_Customers?$orderby=LastName asc
GET /odata/AdventureWorks_Customers?$top=10&$skip=20
GET /odata/AdventureWorks_Customers?$count=true

OData query options ($filter, $orderby, $top, $skip, $count) are handled by Microsoft.AspNetCore.OData middleware — all standard OData filter operators are supported.

Logical combinators and / or and parenthesized grouping are supported (and binds tighter than or):

$filter=Color eq 'Red' and ListPrice gt 100
$filter=Color eq 'Red' or Color eq 'Blue'
$filter=(LastName eq 'Miller' or LastName eq 'Smith') and FirstName eq 'John'

SignalR

Hub URL: /sqlDataHub (configurable via MapSqlDataProviderHub("/custom-path"))

Connection
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/sqlDataHub")
    .build();
await connection.start();
Hub Method: ExecuteQuery
ExecuteQuery(profileName, endpointName, filter?, sorting?, projection?, skip?, take?)
  -> { data, totalCount, success } | { error, success }
Parameter Type Description
profileName string Profile name from config
endpointName string Endpoint name within the profile
filter object (nullable) Recursive filter tree (see below)
sorting array (nullable) Array of { column: string, ascending: bool } objects
projection array (nullable) Array of column name strings to return
skip int (nullable) Number of rows to skip (for paging)
take int (nullable) Max rows to return (default: 50 if omitted)

Filter format: A FilterNode is either a leaf or a combinator:

  • Leaf: { column: "Name", operator: "=", value: "Miller" }
  • AND: { and: [{ column: "A", ... }, { column: "B", ... }] }
  • OR: { or: [{ column: "A", ... }, { column: "B", ... }] }
  • Nested: { and: [{ column: "A", ... }, { or: [{ column: "B", ... }, { column: "C", ... }] }] }

Supported filter operators: =, !=, <>, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN

Examples
// Simple query (returns first 50 rows by default)
const result = await connection.invoke(
    "ExecuteQuery", "AdventureWorks", "Customers",
    null, null, null, null, null
);

// Single filter
const filtered = await connection.invoke(
    "ExecuteQuery", "AdventureWorks", "Customers",
    { column: "LastName", operator: "=", value: "Miller" },
    null, null, null, null
);

// OR filter
const redOrBlue = await connection.invoke(
    "ExecuteQuery", "AdventureWorks", "Products",
    { or: [
        { column: "Color", operator: "=", value: "Red" },
        { column: "Color", operator: "=", value: "Blue" }
    ]},
    null, null, null, null
);

// With sorting and paging
const page2 = await connection.invoke(
    "ExecuteQuery", "AdventureWorks", "Customers",
    null,
    [{ column: "LastName", ascending: true }],
    null,
    20,   // skip
    20    // take
);
// page2.totalCount = 847, page2.data contains rows 21-40

gRPC

Service Definition
service SqlDataProviderGrpc {
  rpc ExecuteQuery (QueryRequest) returns (QueryResponse);
  rpc GetCatalog (CatalogRequest) returns (CatalogResponse);
}
GetCatalog — Discover Available Endpoints

Returns all profiles, their endpoints, discovered column schemas, and allowed column lists.

var catalog = await client.GetCatalogAsync(new CatalogRequest());
foreach (var profile in catalog.Profiles)
{
    Console.WriteLine($"Profile: {profile.Name}");
    foreach (var endpoint in profile.Endpoints)
    {
        Console.WriteLine($"  Endpoint: {endpoint.Name}");
        foreach (var col in endpoint.Columns)
            Console.WriteLine($"    Column: {col.Name} ({col.ClrType}, nullable={col.Nullable})");
    }
}
ExecuteQuery — Run a Query
Field Type Description
profile_name string Profile name from config
endpoint_name string Endpoint name within the profile
filter FilterNode Recursive filter tree (leaf or AND/OR combinator)
sorting repeated SortDescriptor Zero or more sort terms
projection repeated string Column names to return (empty = all columns)
skip optional int32 Number of rows to skip
take optional int32 Max rows to return (default: 50)

FilterNode is recursive — each node is either a leaf (column/operator/value) or a combinator (repeated FilterNode and / repeated FilterNode or).

Response contains success, error, json_data (JSON-serialized row array), and total_count.

Supported filter operators: =, !=, <>, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN

Examples
// Simple query
var response = await client.ExecuteQueryAsync(new QueryRequest {
    ProfileName = "AdventureWorks",
    EndpointName = "Customers"
});

// Single filter
var filtered = await client.ExecuteQueryAsync(new QueryRequest {
    ProfileName = "AdventureWorks",
    EndpointName = "Customers",
    Filter = new FilterNode { Column = "LastName", Operator = "=", Value = "Miller" }
});

// OR filter
var redOrBlue = await client.ExecuteQueryAsync(new QueryRequest {
    ProfileName = "AdventureWorks",
    EndpointName = "Products",
    Filter = new FilterNode { Or = {
        new FilterNode { Column = "Color", Operator = "=", Value = "Red" },
        new FilterNode { Column = "Color", Operator = "=", Value = "Blue" }
    }}
});

// With sorting and paging
var page2 = await client.ExecuteQueryAsync(new QueryRequest {
    ProfileName = "AdventureWorks",
    EndpointName = "Customers",
    Sorting = { new SortDescriptor { Column = "CustomerID", Ascending = true } },
    Skip = 20,
    Take = 20
});

MCP (Model Context Protocol)

Exposes SQL endpoints as tools for AI assistants (Claude Desktop, VS Code, etc.).

Setup
// In Program.cs
builder.Services.AddMcpServer()
    .AddMcpSqlDataProvider();
builder.Services.AddDbConnectionFactory("sqlite", new SqliteConnectionFactory());

var app = builder.Build();
app.MapMcpSqlDataProvider();  // HTTP transport
app.Run();
Configuration for AI Tools

Claude Desktop / Claude Code (HTTP):

{
  "mcpServers": {
    "sql-data-provider": {
      "type": "http",
      "url": "http://localhost:5000/mcp"
    }
  }
}

Claude Desktop (stdio, auto-starts):

{
  "mcpServers": {
    "sql-data-provider": {
      "command": "dotnet",
      "args": ["run", "--project", "/path/to/your/mcp-host-project"]
    }
  }
}
Available Tools

ListEndpoints — Discover all profiles, endpoints, and column schemas. Call this first.

DescribeEndpoint — Get detailed column info for a specific endpoint.

  • profileName — Profile name (e.g. "AdventureWorks")
  • endpointName — Endpoint name (e.g. "Customers")

ExecuteQuery — Run a query with optional filtering, sorting, projection, and paging.

  • profileName — Profile name
  • endpointName — Endpoint name
  • filter — Recursive JSON filter object (see below)
  • orderBy — JSON array: [{"column":"LastName","ascending":true}]
  • select — JSON array: ["CustomerID","FirstName","LastName"]
  • skip — Rows to skip (default: 0)
  • take — Max rows (default: 50)

Filter format: A filter is either a leaf or a combinator, nestable:

  • Leaf: {"column":"LastName","operator":"=","value":"Miller"}
  • AND: {"and":[{"column":"A","operator":"=","value":"1"},{"column":"B","operator":">","value":100}]}
  • OR: {"or":[{"column":"Color","operator":"=","value":"Red"},{"column":"Color","operator":"=","value":"Blue"}]}
  • Nested: {"and":[{"column":"ListPrice","operator":">","value":100},{"or":[{"column":"Color","operator":"=","value":"Red"},{"column":"Color","operator":"=","value":"Blue"}]}]}

All errors include detailed messages with expected format and examples to help AI self-correct.

Authorization

Endpoints support authorization using ASP.NET Core's standard policy-based authorization. Each endpoint can optionally reference a named policy. The framework calls IAuthorizationService.AuthorizeAsync with the policy name and passes any configured parameters as the resource — consumers own the policy definitions and handlers entirely.

Configuration

{
  "EndPointName": "Customers",
  "SqlQuery": "SELECT CustomerID, FirstName, LastName FROM Customer",
  "AuthorizationPolicy": {
    "PolicyName": "HasPermission",
    "Parameters": { "Permission": "customers.read" }
  }
}
Property Default Description
AuthorizationPolicy null Named policy to evaluate. null = no authorization required.
AuthorizationPolicy.PolicyName The policy name registered via AddPolicy at startup.
AuthorizationPolicy.Parameters null Optional IConfigurationSection passed as the resource to AuthorizeAsync. Supports arbitrarily complex structures; handlers can use section.Get<T>().

Simple example (built-in policy)

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdmin", policy => policy.RequireRole("Admin"));
});

No custom handler needed — just reference "RequireAdmin" as the PolicyName in config.

Advanced example (with parameters)

// Requirement (marker class)
public class PermissionRequirement : IAuthorizationRequirement { }

// Register policy
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("HasPermission", policy =>
        policy.Requirements.Add(new PermissionRequirement()));
});
builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

// Handler receives config parameters as the resource
public class PermissionHandler : AuthorizationHandler<PermissionRequirement, IConfigurationSection>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement,
        IConfigurationSection parameters)
    {
        var permission = parameters["Permission"];
        if (context.User.HasClaim("permission", permission))
            context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

Authorization is enforced across all 5 protocols (GraphQL, OData, SignalR, gRPC, MCP). Endpoints without AuthorizationPolicy are accessible without authorization.

Multi-Database Setup

Each profile specifies a ProviderName that maps to a registered IDbConnectionFactory. The factory's Dialect property controls SQL generation (paging syntax, column quoting):

{
  "SqlRetrievalProfiles": [
    {
      "ProfileName": "Sales",
      "ConnectionString": "Server=sales-server;...",
      "ProviderName": "mssql",
      "RetrievalEndpointProfiles": [...]
    },
    {
      "ProfileName": "Archive",
      "ConnectionString": "Host=archive-server;Database=archive;...",
      "ProviderName": "postgres",
      "RetrievalEndpointProfiles": [...]
    }
  ]
}
builder.Services.AddDbConnectionFactory("mssql", new SqlClientConnectionFactory());   // SqlServerDialect
builder.Services.AddDbConnectionFactory("postgres", new NpgsqlConnectionFactory());   // custom dialect

Custom Connection Factory

Implement IDbConnectionFactory for any ADO.NET provider:

using MyDevTime.SqlDataProvider.Connection;
using MyDevTime.SqlDataProvider.Dialect;

public class NpgsqlConnectionFactory : IDbConnectionFactory
{
    public ISqlDialect Dialect => PostgreSqlDialect.Instance;

    public DbConnection CreateConnection(string connectionString)
        => new NpgsqlConnection(connectionString);
}

Built-in dialects: AnsiSqlDialect (default), SqlServerDialect, PostgreSqlDialect, MySqlDialect.

Build & Test

dotnet build            # Build entire solution
dotnet test             # Run all 439 tests across 6 projects
dotnet pack             # Package all libraries as NuGet

Target frameworks: net8.0, net9.0, net10.0. Solution uses .slnx format.

License

Apache 2.0 — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.1 109 3/27/2026
1.7.0 99 3/22/2026
1.6.3 98 3/22/2026
1.6.2 97 3/21/2026
1.6.0 97 3/21/2026