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¶
Output: sdk/typescript/generated/
Generate Python SDK¶
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]
- TypeScanner - Uses Roslyn
MetadataLoadContextto scan assemblies without loading into current AppDomain - Extract Metadata - Reads
[DataContract]attributes for type names and namespaces - Language-Agnostic Model - Creates
ScannedTypeandTypeInfoobjects - Type Mapping - Language-specific mappers convert C# types to target language
- 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:
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:
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)
Related Documentation¶
- ADR-007: Multi-Language SDK Auto-Generation - Architectural decisions and design rationale
- TypeScript SDK README - TypeScript SDK usage guide
- Python SDK README - Python SDK usage guide
- Common Tasks - Adding queries and commands
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.