Skip to content

SDK Generation

The LBS Foundry platform provides auto-generated client SDKs in multiple languages, ensuring type safety and API consistency across all client applications.

Overview

The SDK generator (LBS.Tools.SdkGenerator) automatically generates client libraries from C# domain types:

  • TypeScript SDK - For web frontends (React, Vue, etc.)
  • Python SDK - For scripts, backend integrations, and data science workflows

Key Benefits

  • Always in sync - Generated from the same C# types used by the backend
  • Full type safety - Compile-time validation in TypeScript and Python type hints
  • Zero maintenance - No manual type definitions to keep updated
  • Consistent API - Same builder patterns across all languages

Quick Start

Generate TypeScript SDK

# Windows
.\scripts\generate-typescript-sdk.ps1

# Linux/macOS
./scripts/generate-typescript-sdk.sh

Output: sdk/typescript/generated/

Generate Python SDK

# Windows
.\scripts\generate-python-sdk.ps1

# Linux/macOS
./scripts/generate-python-sdk.sh

Output: sdk/python/src/foundry_sdk/generated/

Architecture

Component Overview

LBS.Tools.SdkGenerator/
├── ICodeGenerator.cs          # Language abstraction interface
├── TypeScanner.cs             # Scans C# assemblies for types
├── TypeMapper.cs              # Maps C# → TypeScript types
├── TypeScriptGenerator.cs     # Generates .ts files
├── PythonTypeMapper.cs        # Maps C# → Python types
├── PythonGenerator.cs         # Generates .py files
└── Program.cs                 # CLI entry point

How It Works

graph LR
    A[C# Assemblies] --> B[TypeScanner]
    B --> C[ScannedType Objects]
    C --> D{ICodeGenerator}
    D --> E[TypeScriptGenerator]
    D --> F[PythonGenerator]
    E --> G[.ts files]
    F --> H[.py files]
  1. TypeScanner - Uses Roslyn MetadataLoadContext to scan assemblies without loading into current AppDomain
  2. Extract Metadata - Reads [DataContract] attributes for type names and namespaces
  3. Language-Agnostic Model - Creates ScannedType and TypeInfo objects
  4. Type Mapping - Language-specific mappers convert C# types to target language
  5. Code Generation - Generates interfaces/dataclasses and builder functions

Type Mapping

C# Type TypeScript Python
string string str
int, long number int
decimal, double number float
bool boolean bool
DateTime string datetime
Guid string str
List<T> T[] list[T]
Dictionary<K,V> Record<K, V> dict[K, V]
T? (nullable) T \| null T \| None

Generated Code Structure

TypeScript Output

sdk/typescript/generated/
├── types.ts                    # All query/command/import interfaces
├── index.ts                    # Main exports
└── modules/
    ├── core/
    │   ├── queries.ts         # Query builder functions
    │   ├── commands.ts        # Command builder functions
    │   ├── imports.ts         # Import builder functions
    │   └── index.ts
    ├── sport/
    ├── superCoach/
    └── ballr/

Example Generated Type:

export interface PlayerSearch {
  queryType: 'PlayerSearch';
  teams: string[] | null;
  searchTerm: string | null;
  minPrice: number;
  maxPrice: number;
  positions: string[] | null;
  playersToIncludeInResults: string[];
}

Example Query Builder:

export const queries = {
  playerSearch: (params: Omit<PlayerSearch, 'queryType'>): PlayerSearch => ({
    queryType: 'PlayerSearch',
    ...params
  }),
} as const;

Example Import Builder:

export const imports = {
  userSeeding: (data: UserSeedingImport): UserSeedingImport => data,
} as const;

Python Output

sdk/python/src/foundry_sdk/generated/
├── types.py                    # All dataclasses
├── __init__.py                 # Main exports
└── modules/
    ├── core/
    │   ├── queries.py         # Query builder functions
    │   ├── commands.py        # Command builder functions
    │   ├── imports.py         # Import builder functions
    │   └── __init__.py
    ├── sport/
    ├── super_coach/           # snake_case module names
    └── ballr/

Example Generated Type:

@dataclass
class PlayerSearch:
    """Auto-generated dataclass."""
    query_type: str = 'PlayerSearch'
    teams: list[str] | None
    search_term: str | None
    min_price: int
    max_price: int
    positions: list[str] | None
    players_to_include_in_results: list[str]

Example Query Builder:

def player_search(
    teams: list[str] | None,
    search_term: str | None,
    min_price: int,
    max_price: int,
    positions: list[str] | None,
    players_to_include_in_results: list[str]
) -> PlayerSearch:
    """Create a PlayerSearch instance."""
    return PlayerSearch(
        query_type='PlayerSearch',
        teams=teams,
        search_term=search_term,
        min_price=min_price,
        max_price=max_price,
        positions=positions,
        players_to_include_in_results=players_to_include_in_results,
    )

Example Import Builder:

def user_seeding(
    enabled: bool,
    skip_if_users_exist: bool,
    users: list[UserSeedData]
) -> UserSeedingImport:
    """Create a UserSeedingImport instance."""
    return UserSeedingImport(
        enabled=enabled,
        skip_if_users_exist=skip_if_users_exist,
        users=users,
    )

Usage Examples

TypeScript

Query Example:

import { FoundryClient } from '@luckboxstudios/foundry-sdk';
import { queries } from '@luckboxstudios/foundry-sdk/supercoach';

const client = new FoundryClient({
  baseUrl: 'https://api.foundry.com',
  authToken: 'your-bearer-token'
});

const result = await client.query(queries.playerSearch({
  searchTerm: 'Smith',
  minPrice: 100000,
  maxPrice: 500000,
  teams: [],
  positions: [],
  playersToIncludeInResults: []
}));

Import Example:

import { FoundryClient } from '@luckboxstudios/foundry-sdk';
import { imports } from '@luckboxstudios/foundry-sdk/core';

const client = new FoundryClient({
  baseUrl: 'https://api.foundry.com',
  authToken: 'your-bearer-token'
});

const result = await client.import('UserSeedingImport', imports.userSeeding({
  enabled: true,
  skipIfUsersExist: true,
  users: [
    { email: 'user@example.com', displayName: 'Test User', roles: ['Member'] }
  ]
}));

console.log(`Imported ${result.commandsExecuted} commands`);

Python

Query Example:

import asyncio
from foundry_sdk import FoundryClient, FoundryClientConfig
from foundry_sdk.generated.modules.super_coach.queries import player_search

async def main():
    client = FoundryClient(FoundryClientConfig(
        base_url="https://api.foundry.com",
        auth_token="your-bearer-token"
    ))

    result = await client.query(player_search(
        search_term="Smith",
        min_price=100000,
        max_price=500000,
        teams=None,
        positions=None,
        players_to_include_in_results=[]
    ))

    if result.success:
        print(f"Found players: {result.data}")

asyncio.run(main())

Import Example:

import asyncio
from foundry_sdk import FoundryClient, FoundryClientConfig
from foundry_sdk.generated.modules.core.imports import user_seeding

async def main():
    client = FoundryClient(FoundryClientConfig(
        base_url="https://api.foundry.com",
        auth_token="your-bearer-token"
    ))

    result = await client.import_data(
        "UserSeedingImport",
        user_seeding(
            enabled=True,
            skip_if_users_exist=True,
            users=[
                {"email": "user@example.com", "display_name": "Test User", "roles": ["Member"]}
            ]
        )
    )

    if result.success:
        print(f"Imported {result.commands_executed} commands")

asyncio.run(main())

CLI Reference

dotnet run --project src/Tools/LBS.Tools.SdkGenerator/LBS.Tools.SdkGenerator.csproj -- \
  <assembly-dir> \
  <reference-dir> \
  <output-dir> \
  --language <typescript|python>

Parameters: - assembly-dir - Directory containing assemblies to scan (or single .dll file) - reference-dir - Directory containing .NET reference assemblies - output-dir - Output directory for generated SDK - --language - Target language (typescript or python)

Example:

dotnet run --project src/Tools/LBS.Tools.SdkGenerator/LBS.Tools.SdkGenerator.csproj -- `
  "src/Domain/LBS.Ballr.Core/bin/Debug/net10.0/LBS.Ballr.Core.dll" `
  "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\10.0.0\ref\net10.0" `
  "sdk/typescript/generated" `
  --language typescript

Adding a New Language

To add support for a new language (e.g., C#, Go, Rust):

1. Create Type Mapper

// src/Tools/LBS.Tools.SdkGenerator/GoTypeMapper.cs
public class GoTypeMapper
{
    private readonly Dictionary<string, string> primitiveTypeMap = new()
    {
        { "System.String", "string" },
        { "System.Int32", "int" },
        { "System.Boolean", "bool" },
        // ... more mappings
    };

    public string MapType(TypeInfo typeInfo, bool isNullable)
    {
        var baseType = this.GetGoType(typeInfo);
        return isNullable ? $"*{baseType}" : baseType;
    }

    private string GetGoType(TypeInfo typeInfo)
    {
        // Handle arrays
        if (typeInfo.IsArray && typeInfo.ElementType != null)
        {
            return $"[]{this.GetGoType(typeInfo.ElementType)}";
        }

        // Handle primitives
        if (this.primitiveTypeMap.TryGetValue(typeInfo.FullTypeName, out var goType))
        {
            return goType;
        }

        return typeInfo.FullTypeName.Split('.').Last();
    }
}

2. Create Generator

// src/Tools/LBS.Tools.SdkGenerator/GoGenerator.cs
public class GoGenerator : ICodeGenerator
{
    private readonly GoTypeMapper typeMapper;

    public string FileExtension => ".go";
    public string LanguageName => "Go";

    public GoGenerator()
    {
        this.typeMapper = new GoTypeMapper();
    }

    public GeneratedOutput Generate(List<ScannedType> types)
    {
        // Generate Go structs and functions
        // ...
    }
}

3. Register in Program.cs

ICodeGenerator generator = language switch
{
    "typescript" => new TypeScriptGenerator(),
    "python" => new PythonGenerator(),
    "go" => new GoGenerator(),  // Add here
    _ => throw new InvalidOperationException($"Unsupported language: {language}")
};

4. Create Generation Script

# scripts/generate-go-sdk.ps1
dotnet run --project src/Tools/LBS.Tools.SdkGenerator/LBS.Tools.SdkGenerator.csproj -- `
  "src/Domain/LBS.Ballr.Core/bin/Debug/net10.0/LBS.Ballr.Core.dll" `
  "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\10.0.0\ref\net10.0" `
  "sdk/go/generated" `
  --language go

Controlling SDK Output

DataContract Namespace

Use [DataContract] attributes to control SDK organization:

// Query goes into SuperCoach module
[DataContract(Name = "PlayerSearch", Namespace = LbsNamespace.SuperCoachQueries)]
public class PlayerSearchQuery : IPublicQuery
{
    public string QueryType => this.GetType().GetQueryTypeName();
    public string? SearchTerm { get; set; }
    // ...
}

// Import goes into Core module
[DataContract(Name = "UserSeedingImport", Namespace = LbsNamespace.CoreImports)]
public sealed class UserSeedingConfiguration
{
    [DataMember]
    public bool Enabled { get; set; } = true;
    [DataMember]
    public List<UserSeedData> Users { get; set; } = [];
    // ...
}

Namespace Constants (from LBSNamespace.cs):

public static class LbsNamespace
{
    // Query Namespaces
    public const string CoreQueries = "LBS.Foundry.Queries.Core";
    public const string SportQueries = "LBS.Foundry.Queries.Sport";
    public const string SuperCoachQueries = "LBS.Foundry.Queries.SuperCoach";
    public const string BallrQueries = "LBS.Foundry.Queries.Ballr";
    public const string DiscordQueries = "LBS.Foundry.Queries.Discord";

    // Command Namespaces
    public const string CoreCommands = "LBS.Foundry.Commands.Core";
    public const string SportCommands = "LBS.Foundry.Commands.Sport";
    public const string SuperCoachCommands = "LBS.Foundry.Commands.SuperCoach";
    public const string BallrCommands = "LBS.Foundry.Commands.Ballr";
    public const string DiscordCommands = "LBS.Foundry.Commands.Discord";

    // Import Namespaces
    public const string CoreImports = "LBS.Foundry.Imports.Core";
    public const string SportImports = "LBS.Foundry.Imports.Sport";
    public const string SuperCoachImports = "LBS.Foundry.Imports.SuperCoach";
    public const string BallrImports = "LBS.Foundry.Imports.Ballr";
    public const string DiscordImports = "LBS.Foundry.Imports.Discord";
}

Excluding Properties

Properties named QueryType, CommandType, or User are automatically excluded from generation.

public class PlayerSearchQuery : IPublicQuery, IRequiresUserContext
{
    public string QueryType => this.GetType().GetQueryTypeName();  // Excluded
    public ClaimsPrincipal? User => UserContext.Current;           // Excluded

    public string? SearchTerm { get; set; }                        // Included
    public int MinPrice { get; set; }                              // Included
}

CI/CD Integration

GitHub Actions Workflow

TypeScript SDK is automatically published on domain type changes:

Workflow: .github/workflows/Build-Container-Apps.yml

Triggers: - Push to main branch - Changes in src/Domain/**/*.cs - Manual workflow dispatch

Steps: 1. Build solution 2. Generate TypeScript SDK 3. Install npm dependencies 4. Build TypeScript 5. Version with timestamp (0.YYMMDD.HHMM.RUN_NUMBER) 6. Publish to GitHub Packages

Python SDK Publishing

Python SDK publishing workflow can be added similarly:

# .github/workflows/publish-python-sdk.yml
name: Publish Python SDK

on:
  push:
    branches: [main]
    paths:
      - 'src/Domain/**/*.cs'
  workflow_dispatch:

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Generate Python SDK
        run: ./scripts/generate-python-sdk.sh Release

      - name: Build package
        run: |
          cd sdk/python
          python -m pip install build
          python -m build

      - name: Publish to PyPI
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
        run: |
          python -m pip install twine
          python -m twine upload sdk/python/dist/*

Troubleshooting

"Could not find core assembly"

Problem: MetadataLoadContext can't find System.Runtime

Solution: Ensure reference-dir points to valid .NET reference assemblies:

# Windows
C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\10.0.0\ref\net10.0

# Linux/macOS
/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/10.0.0/ref/net10.0

"Found 0 types"

Problem: Scanner isn't finding types with [DataContract] attributes

Solution: 1. Ensure types are public and not abstract 2. Check types implement IQuery or inherit from DomainCommand 3. Verify [DataContract(Name = "...")] attribute is present 4. Check assembly is in the assembly-dir path

Import Path Errors (Python)

Problem: Generated Python code has incorrect relative imports

Solution: Ensure module structure follows:

foundry_sdk/
├── generated/
│   ├── types.py
│   └── modules/
│       └── super_coach/
│           └── queries.py  # Should use: from ...types import *

Type Mapping Issues

Problem: Unknown C# type generates any (TypeScript) or str (Python)

Solution: Add mapping to TypeMapper.cs or PythonTypeMapper.cs:

// Add to primitiveTypeMap dictionary
{ "YourNamespace.CustomType", "CustomTypeInTargetLanguage" }

Best Practices

1. Always Use DataContract Attributes

// Good - Will be included in SDK
[DataContract(Name = "PlayerSearch", Namespace = LbsNamespace.SuperCoachQueries)]
public class PlayerSearchQuery : IPublicQuery { }

// Bad - Will be skipped
public class PlayerSearchQuery : IPublicQuery { }

2. Use Meaningful Names

// Good - Clear, descriptive name
[DataContract(Name = "PlayerSearch")]

// Bad - Includes implementation details
[DataContract(Name = "PlayerSearchQuery")]

The SDK generator automatically strips "Query" and "Command" suffixes from builder function names.

3. Group by Feature Module

// SuperCoach queries
[DataContract(Name = "PlayerSearch", Namespace = LbsNamespace.SuperCoachQueries)]

// Sport queries
[DataContract(Name = "TeamList", Namespace = LbsNamespace.SportQueries)]

// Core imports
[DataContract(Name = "UserSeedingImport", Namespace = LbsNamespace.CoreImports)]

This creates logical module grouping in generated SDKs.

4. Regenerate After Schema Changes

Always regenerate SDKs after modifying query/command/import types:

.\scripts\generate-typescript-sdk.ps1
.\scripts\generate-python-sdk.ps1

5. Version SDKs Properly

Use semantic versioning for published packages: - Major - Breaking changes to generated types - Minor - New types/properties added - Patch - Bug fixes in client libraries (not generated code)

Summary

The multi-language SDK generator ensures type safety and consistency across all client applications:

  • Zero Drift - Generated from source of truth (C# types)
  • Type Safe - Compile-time validation in TypeScript and Python
  • Extensible - Easy to add new languages via ICodeGenerator
  • Automated - CI/CD integration for automatic publishing
  • Developer Friendly - Simple scripts for local generation

For architectural details and design decisions, see ADR-007.