Setup & Configuration
Granit.AI brings Large Language Model (LLM) capabilities to your application through a
provider-agnostic abstraction layer built on Microsoft.Extensions.AI (IChatClient,
IEmbeddingGenerator). AI providers are interchangeable per environment — Ollama for
development, Azure OpenAI for production — without changing application code.
The module introduces workspaces: named AI configurations that bind a provider, model, system prompt, and parameters. Workspaces are multi-tenant, permission-scoped, and can be declared in code (immutable) or managed via API (dynamic). Every AI interaction is tracked for cost monitoring and ISO 27001 compliance.
Why Microsoft.Extensions.AI?
Section titled “Why Microsoft.Extensions.AI?”| Approach | Pros | Cons |
|---|---|---|
| Direct SDK usage (OpenAI, Anthropic) | Full API access | Vendor lock-in, no middleware pipeline, each module reimplements integration |
Custom abstractions (ITextGenerationService) | Full control | Isolated from .NET ecosystem, reinvents the wheel |
| Microsoft.Extensions.AI ✓ | Standard .NET 10, ~100 compatible packages, middleware pipeline (logging, caching, OTel) | Newer API surface |
Granit.AI uses the .NET standard. Your application code depends on IChatClient, not on
OpenAI or Anthropic. Swapping providers is a one-line configuration change.
Package structure
Section titled “Package structure”DirectoryGranit.AI/ Core abstractions, workspace management, factory pattern
- Granit.AI.OpenAI OpenAI provider (GPT-4o, o3/o4-mini, embeddings)
- Granit.AI.AzureOpenAI Azure OpenAI with Managed Identity support
- Granit.AI.Anthropic Anthropic Claude (Opus, Sonnet, Haiku)
- Granit.AI.Ollama Local models via OllamaSharp (dev, on-premise, GDPR)
- Granit.AI.EntityFrameworkCore Isolated DbContext for workspaces and usage tracking
- Granit.AI.Extraction Structured document extraction (PDF → typed C# object)
- Granit.AI.VectorData Multi-tenant vector storage for semantic search (RAG)
DirectoryGranit.DataExchange.AI/ AI-powered column mapping for CSV/Excel import
- …
DirectoryGranit.Querying.AI/ Natural Language Query — phrases to structured filters
- …
| Package | Role | Depends on |
|---|---|---|
Granit.AI | IAIChatClientFactory, IAIProviderFactory, workspace CQRS, usage tracking | Granit.Core |
Granit.AI.OpenAI | OpenAI provider via Microsoft.Extensions.AI.OpenAI | Granit.AI |
Granit.AI.AzureOpenAI | Azure OpenAI with API key + DefaultAzureCredential | Granit.AI |
Granit.AI.Anthropic | Claude models via official Anthropic SDK | Granit.AI |
Granit.AI.Ollama | Local models, health check, zero API key | Granit.AI |
Granit.AI.EntityFrameworkCore | AIDbContext, EfAIWorkspaceStore, EfAIUsageStore | Granit.AI, Granit.Persistence |
Granit.AI.Extraction | IDocumentExtractor<T>, structured output from documents | Granit.AI |
Granit.AI.VectorData | IVectorCollection<T>, ISemanticSearchService for RAG | Granit.AI |
Granit.DataExchange.AI | AI-powered ISemanticMappingService for import mapping | Granit.AI, Granit.DataExchange |
Granit.Querying.AI | INaturalLanguageQueryTranslator (NLQ → QueryRequest) | Granit.AI, Granit.Querying |
Provider comparison
Section titled “Provider comparison”| Provider | Models | Embeddings | Auth | Best for |
|---|---|---|---|---|
| OpenAI | GPT-4o, o3, o4-mini | text-embedding-3 | API key | General-purpose, global deployments |
| Azure OpenAI | Same as OpenAI | Same | API key / Managed Identity | EU data sovereignty, HDS, enterprise |
| Anthropic | Claude Opus, Sonnet, Haiku | Not supported | API key | Long document analysis, reasoning |
| Ollama | Llama 3.1, Mistral, Phi, Gemma | nomic-embed-text | None | Development, on-premise, GDPR-strict |
Dependency graph
Section titled “Dependency graph”graph TD
AI[Granit.AI] --> CO[Granit.Core]
OAI[Granit.AI.OpenAI] --> AI
AAI[Granit.AI.AzureOpenAI] --> AI
AN[Granit.AI.Anthropic] --> AI
OL[Granit.AI.Ollama] --> AI
EF[Granit.AI.EntityFrameworkCore] --> AI
EF --> P[Granit.Persistence]
EX[Granit.AI.Extraction] --> AI
VD[Granit.AI.VectorData] --> AI
DEX[Granit.DataExchange.AI] --> AI
DEX --> DE[Granit.DataExchange]
QAI[Granit.Querying.AI] --> AI
QAI --> Q[Granit.Querying]
[DependsOn(typeof(GranitAIOllamaModule))]public class AppModule : GranitModule { }builder.AddGranitAI();builder.AddGranitAIOllama();No configuration needed — connects to http://localhost:11434 with llama3.1 by default.
# Start Ollama and pull the default modelollama pull llama3.1ollama serve[DependsOn(typeof(GranitAIOpenAIModule))]public class AppModule : GranitModule { }builder.AddGranitAI();builder.AddGranitAIOpenAI();{ "AI": { "OpenAI": { "ApiKey": "<from-vault>", "DefaultModel": "gpt-4o", "DefaultEmbeddingModel": "text-embedding-3-small" } }}[DependsOn(typeof(GranitAIAzureOpenAIModule))]public class AppModule : GranitModule { }builder.AddGranitAI();builder.AddGranitAIAzureOpenAI();{ "AI": { "AzureOpenAI": { "Endpoint": "https://my-resource.openai.azure.com", "DefaultDeployment": "gpt-4o", "DefaultEmbeddingDeployment": "text-embedding-3-small" } }}For Managed Identity (recommended in production), omit ApiKey — DefaultAzureCredential
is used automatically.
[DependsOn(typeof(GranitAIAnthropicModule))]public class AppModule : GranitModule { }builder.AddGranitAI();builder.AddGranitAIAnthropic();{ "AI": { "Anthropic": { "ApiKey": "<from-vault>", "DefaultModel": "claude-sonnet-4-6" } }}Adding persistence
Section titled “Adding persistence”For production, add Granit.AI.EntityFrameworkCore to persist workspaces and track usage:
[DependsOn( typeof(GranitAIOllamaModule), // or any provider typeof(GranitAIEntityFrameworkCoreModule))]public class AppModule : GranitModule { }builder.AddGranitAI();builder.AddGranitAIOllama();builder.AddGranitAIEntityFrameworkCore(options => options.UseNpgsql(builder.Configuration.GetConnectionString("AI")));Workspace architecture
Section titled “Workspace architecture”A workspace is a named AI configuration: which provider, which model, what parameters.
flowchart LR
App[Application Code] -->|"CreateAsync('support-chat')"| F[IAIChatClientFactory]
F --> WP[IAIWorkspaceProvider]
WP --> SYS[System Workspaces<br/>code-defined, immutable]
WP --> DYN[Dynamic Workspaces<br/>database, API-managed]
F --> PF[IAIProviderFactory]
PF --> OAI[OpenAI]
PF --> AAI[Azure OpenAI]
PF --> AN[Anthropic]
PF --> OL[Ollama]
F -->|returns| CC[IChatClient]
System workspaces (code-defined)
Section titled “System workspaces (code-defined)”Declare workspaces in code for configurations that should not change at runtime:
public class AppWorkspaceDefinitionProvider : IAIWorkspaceDefinitionProvider{ public void Define(IAIWorkspaceDefinitionContext context) { context.Add(new AIWorkspace { Name = "document-extraction", Provider = "AzureOpenAI", Model = "gpt-4o", SystemPrompt = "Extract structured data from documents. Return valid JSON only.", Temperature = 0.0f, MaxOutputTokens = 4096, });
context.Add(new AIWorkspace { Name = "support-chat", Provider = "Anthropic", Model = "claude-sonnet-4-6", SystemPrompt = "You are a helpful customer support assistant.", Temperature = 0.7f, }); }}System workspaces take precedence over dynamic workspaces with the same name and cannot be modified or deleted via the API.
Dynamic workspaces (database-managed)
Section titled “Dynamic workspaces (database-managed)”Dynamic workspaces are created and managed through IAIWorkspaceManager (CQRS writer)
and stored by Granit.AI.EntityFrameworkCore. Use these when workspace configuration
should be editable at runtime (e.g., per-tenant customization).
Using AI in your modules
Section titled “Using AI in your modules”Chat completion
Section titled “Chat completion”public class InvoiceSummaryService(IAIChatClientFactory chatClientFactory){ public async Task<string> SummarizeAsync( Invoice invoice, CancellationToken cancellationToken) { IChatClient client = await chatClientFactory .CreateAsync("document-extraction", cancellationToken) .ConfigureAwait(false);
ChatResponse response = await client.GetResponseAsync( $"Summarize this invoice in 3 bullet points: {invoice.Description}", cancellationToken: cancellationToken) .ConfigureAwait(false);
return response.Text; }}Embedding generation
Section titled “Embedding generation”public class PatientSearchService(IAIEmbeddingGeneratorFactory embeddingFactory){ public async Task<Embedding<float>> EmbedAsync( string searchQuery, CancellationToken cancellationToken) { IEmbeddingGenerator<string, Embedding<float>> generator = await embeddingFactory .CreateAsync("embeddings", cancellationToken) .ConfigureAwait(false);
Embedding<float> embedding = await generator .GenerateEmbeddingAsync(searchQuery, cancellationToken: cancellationToken) .ConfigureAwait(false);
return embedding; }}Graceful degradation
Section titled “Graceful degradation”When no provider is registered, IAIChatClientFactory.CreateAsync() throws
AIProviderNotRegisteredException with a clear message indicating which package to install.
When no persistence adapter is configured, null implementations are used — workspaces work
from code definitions only, and usage records are discarded.
Usage tracking and cost monitoring
Section titled “Usage tracking and cost monitoring”Every IChatClient call is automatically tracked when GranitAIOptions.EnableUsageTracking
is true (default). The AIUsageRecord captures:
| Field | Description |
|---|---|
TenantId | Tenant that made the request |
UserId | User who triggered the AI call |
WorkspaceName | Workspace used |
Provider / Model | Provider and model identifier |
InputTokens / OutputTokens | Token consumption |
EstimatedCostUsd | Cost estimate based on configured pricing |
Duration | Response time |
Timestamp | When the interaction occurred (UTC) |
Records are persisted by Granit.AI.EntityFrameworkCore and can be queried for billing
dashboards, cost alerts, and rate limiting.
AI-powered modules
Section titled “AI-powered modules”Each AI integration has a dedicated documentation page with detailed examples, architecture diagrams, risks analysis, and GDPR considerations:
| Module | What it does | Dedicated page |
|---|---|---|
| DataExchange.AI | AI column mapping for CSV/Excel import — handles cryptic columns like Nom_Clt_V2_Final | AI: Import Mapping |
| AI.Extraction | Extract typed C# objects from documents (invoice PDF → InvoiceData record) | AI: Document Extraction |
| Querying.AI | Natural language → structured filters (“unpaid invoices from last week”) | AI: Natural Language Query |
| AI.VectorData | Semantic search / RAG — find documents by meaning, build knowledge bases | AI: Semantic Search & RAG |
Soft dependency pattern
Section titled “Soft dependency pattern”Every AI integration follows the same pattern — the existing module defines an interface with a null-object default. The AI package replaces it via DI:
Granit.{Module} → defines IService + NullService (no-op default) no reference to AI
Granit.{Module}.AI → implements AIService references Granit.{Module} replaces via DI references Granit.AIWithout the AI package: the feature is silently skipped.
With it: the feature activates. No if statements, no feature flags — just DI composition.
Configuration reference
Section titled “Configuration reference”GranitAIOptions (section: AI)
Section titled “GranitAIOptions (section: AI)”| Property | Type | Default | Description |
|---|---|---|---|
DefaultWorkspace | string | "default" | Workspace used when none is specified |
EnableUsageTracking | bool | true | Track token consumption and cost |
EnableAuditTrail | bool | true | Log every AI interaction (ISO 27001) |
OpenAIProviderOptions (section: AI:OpenAI)
Section titled “OpenAIProviderOptions (section: AI:OpenAI)”| Property | Type | Default | Description |
|---|---|---|---|
ApiKey | string | — | OpenAI API key (inject from Vault) |
Endpoint | string? | null | Custom base URL (for proxies) |
DefaultModel | string | "gpt-4o" | Fallback model |
DefaultEmbeddingModel | string | "text-embedding-3-small" | Fallback embedding model |
AzureOpenAIProviderOptions (section: AI:AzureOpenAI)
Section titled “AzureOpenAIProviderOptions (section: AI:AzureOpenAI)”| Property | Type | Default | Description |
|---|---|---|---|
Endpoint | string | — | Azure resource endpoint (HTTPS) |
ApiKey | string? | null | API key; empty = Managed Identity |
DefaultDeployment | string | "gpt-4o" | Fallback deployment name |
DefaultEmbeddingDeployment | string | "text-embedding-3-small" | Fallback embedding deployment |
AnthropicProviderOptions (section: AI:Anthropic)
Section titled “AnthropicProviderOptions (section: AI:Anthropic)”| Property | Type | Default | Description |
|---|---|---|---|
ApiKey | string | — | Anthropic API key (inject from Vault) |
DefaultModel | string | "claude-sonnet-4-6" | Fallback model |
OllamaOptions (section: AI:Ollama)
Section titled “OllamaOptions (section: AI:Ollama)”| Property | Type | Default | Description |
|---|---|---|---|
Endpoint | string | "http://localhost:11434" | Ollama server URL |
DefaultModel | string | "llama3.1" | Fallback model (8B server variant) |
DataExchangeAIOptions (section: AI:DataExchange)
Section titled “DataExchangeAIOptions (section: AI:DataExchange)”| Property | Type | Default | Description |
|---|---|---|---|
WorkspaceName | string | "default" | AI workspace for mapping suggestions |
TimeoutSeconds | int | 10 | LLM call timeout |
MinConfidenceScore | double | 0.6 | Minimum score to accept a suggestion |
IncludePreviewRows | bool | false | Include sample data rows in prompt (GDPR opt-in) |
PreviewRowCount | int | 5 | Number of preview rows when enabled |
ExtractionOptions (section: AI:Extraction)
Section titled “ExtractionOptions (section: AI:Extraction)”| Property | Type | Default | Description |
|---|---|---|---|
WorkspaceName | string | "default" | AI workspace for extraction |
ReviewThreshold | double | 0.7 | Below this confidence → NeedsReview status |
TimeoutSeconds | int | 30 | Extraction timeout (OCR + LLM) |
QueryingAIOptions (section: AI:Querying)
Section titled “QueryingAIOptions (section: AI:Querying)”| Property | Type | Default | Description |
|---|---|---|---|
WorkspaceName | string | "default" | AI workspace for NLQ translation |
TimeoutSeconds | int | 5 | NLQ should be fast — short timeout |
VectorDataOptions (section: AI:VectorData)
Section titled “VectorDataOptions (section: AI:VectorData)”| Property | Type | Default | Description |
|---|---|---|---|
EmbeddingWorkspace | string | "default" | Workspace for embedding generation |
DefaultSearchLimit | int | 10 | Default number of results |
Compliance
Section titled “Compliance”- Data minimization: workspaces control what data reaches the LLM via system prompts
- Right to erasure: workspace and usage records support soft delete
- Data residency: Azure OpenAI supports EU-only deployments; Ollama keeps data on-premise
- PII protection: usage records store tenant/user IDs, not conversation content
ISO 27001
Section titled “ISO 27001”- Audit trail: every AI interaction is recorded with tenant, user, workspace, model, timestamp
- Usage records are immutable:
CreationAuditedEntitybase class, no update/delete operations - Workspace changes are audited:
AuditedEntitywith CreatedAt/By, ModifiedAt/By
Database schema
Section titled “Database schema”When using Granit.AI.EntityFrameworkCore, two tables are created:
| Table | Entity | Purpose |
|---|---|---|
ai_workspaces | AIWorkspaceEntity | Dynamic workspace configurations (soft-deletable, audited) |
ai_usage_records | AIUsageRecordEntity | Token consumption and cost tracking (immutable) |
Key indexes:
uq_ai_workspaces_tenant_name— unique workspace name per tenantix_ai_usage_records_tenant_workspace_date— billing queries by workspace and periodix_ai_usage_records_tenant_provider_model— cost aggregation by provider/model
See also
Section titled “See also”- Data Exchange — import/export module where AI mapping plugs in
- Querying — query engine where NLQ translates natural language
- Persistence — isolated DbContext pattern,
ApplyGranitConventions - Vault & Encryption — secure API key storage with rotation
- Observability — OpenTelemetry integration for AI call tracing
- Multi-tenancy — tenant-scoped workspace isolation