[{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/categories/ai/","section":"Categories","summary":"","title":"AI","type":"categories"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/","section":"Chris Malpass | Dev \u0026 Consultant","summary":"","title":"Chris Malpass | Dev \u0026 Consultant","type":"page"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/tags/developer-tools/","section":"Tags","summary":"","title":"Developer Tools","type":"tags"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/tags/lsp/","section":"Tags","summary":"","title":"LSP","type":"tags"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/tags/mcp/","section":"Tags","summary":"","title":"MCP","type":"tags"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/tags/serena/","section":"Tags","summary":"","title":"Serena","type":"tags"},{"content":"We\u0026rsquo;ve all been there: you ask an LLM to refactor a service, and it confidently hallucination-writes a solution that references non-existent files or misses the scope of a critical variable. It\u0026rsquo;s frustrating because the model is smart, but it\u0026rsquo;s essentially flying blind without a map of your actual codebase.\nSerena\u0026rsquo;s Model Context Protocol (MCP) server is the \u0026ldquo;map\u0026rdquo; we\u0026rsquo;ve been waiting for. By bridging the generative power of LLMs with the deterministic precision of the Language Server Protocol (LSP), Serena transforms passive chat assistants into active, grounded agents capable of surgical code modification.\nWhy This Matters: Determinism over Probability # The industry has leaned heavily on Retrieval-Augmented Generation (RAG) to solve the context problem. RAG is great for \u0026ldquo;How do I do X?\u0026rdquo;, but it\u0026rsquo;s notoriously imprecise for \u0026ldquo;Refactor Y.\u0026rdquo; It operates on probabilistic similarity—guessing what\u0026rsquo;s relevant based on text embeddings.\nSerena shifts the paradigm to Deterministic Navigation. Instead of guessing, it uses the same LSP technology that powers \u0026ldquo;Go to Definition\u0026rdquo; in your IDE. When an agent asks for a function definition, it gets the exact file and line number based on the Abstract Syntax Tree (AST), not a statistical guess.\nHigh-Level Architecture: The Four-Layer Model # Serena isn\u0026rsquo;t just a wrapper; it\u0026rsquo;s a multi-layered system designed to translate natural language intent into precise file system and LSP operations.\nLayer Component Name Primary Functionality Underlying Technology 1. Transport MCP Server Interface Manages JSON-RPC communication (stdio/SSE) with the LLM client. Python MCP SDK, uv 2. Orchestration SerenaAgent The \u0026ldquo;brain.\u0026rdquo; Routes requests, manages context/modes, and enforces security. Python (agent.py) 3. Adaptation Solid-LSP The translation engine. Converts sync tool calls to async LSP messages. Modified multilspy 4. Execution Language Servers \u0026amp; FS The workhorses. Binary executables (e.g., rust-analyzer) and OS I/O. Native Binaries, OS APIs This layering ensures that the LLM doesn\u0026rsquo;t have to worry about the \u0026ldquo;noise\u0026rdquo; of asynchronous IDE events. It just calls a tool and gets a reliable result.\nSolid-LSP: Making LSPs Agent-Friendly # The most technically complex part of Serena is the Solid-LSP framework. Standard LSPs are designed for humans—they are asynchronous and \u0026ldquo;noisy.\u0026rdquo; If you type a character, the server eventually sends back some red squiggles.\nAn AI agent, however, needs synchronous, transactional operations. If it writes code, it needs to know immediately if it compiles or what the new symbol table looks like. Solid-LSP (built on top of the multilspy library) forces this synchronization. It:\nManages Lifecycles: Automatically fetches and boots the right language server (e.g., gopls, pyright, rust-analyzer). Blocks for Consistency: Ensures an operation is 100% complete before returning control to the agent. Virtual Documents: Allows agents to \u0026ldquo;preview\u0026rdquo; refactors in memory before flushing them to the file system. The Tooling Ecosystem: Beyond Search and Replace # Serena exposes over 20 distinct tools to the LLM. Understanding these is key to driving the agent effectively.\n1. Symbolic Navigation # find_symbol(name_path): Uses dot-notation (e.g., module.class.method) to find a definition with 100% precision. get_symbols_overview(): A massive token-saver. It returns a \u0026ldquo;skeleton\u0026rdquo; of a file (signatures only), allowing the agent to understand the API surface without reading 5,000 lines of code. find_referencing_symbols(): Essential for impact analysis. It asks, \u0026ldquo;Who calls this function?\u0026rdquo; before making a change. 2. Semantic Editing # replace_symbol_body(): Targets a symbol\u0026rsquo;s exact byte range. This is robust against formatting changes in surrounding code. rename_symbol(): Executes a workspace-wide rename that respects scope—safer than any regex search-and-replace. 3. Memory \u0026amp; Execution # edit_memory(): Allows the agent to update its own \u0026ldquo;long-term memory\u0026rdquo; in the .serena directory as the project evolves. execute_shell_command(): Closes the loop by allowing the agent to run npm test or dotnet build to verify its own work. Long-Term Memory: The .serena Directory # Unlike transient chat sessions, software development is stateful. Serena mimics a developer\u0026rsquo;s mental model through its persistence layer in the .serena directory.\nWhen you first activate Serena, it triggers an Onboarding Heuristic:\nIdentifies Project: Detects Cargo.toml, pom.xml, etc. Initializes LSP: Downloads and boots the relevant servers. Indexes Workspace: Builds the symbol table. Generates Memories: Creates markdown files like project_overview.md and conventions.md. This \u0026ldquo;long-term memory\u0026rdquo; allows the agent to recall that \u0026ldquo;we use the Repository pattern for data access\u0026rdquo; without re-analyzing the code every time, saving you both tokens and time.\nInstallation \u0026amp; Configuration # Serena is designed to be flexible, supporting everything from ephemeral runs to hermetic Docker environments.\nDeployment Options # uvx (Recommended): The fastest way to get started. It isolates dependencies and ensures you\u0026rsquo;re always on the latest version. 1 uvx --from git+https://github.com/oraios/serena serena start-mcp-server Docker: Best for avoiding \u0026ldquo;dependency hell\u0026rdquo; or running in environments without local toolchains. 1 docker run --rm -i --network host -v /path/to/project:/workspaces/project ghcr.io/oraios/serena:latest GitHub MCP Registry: The easiest way for VS Code users. Provides a one-click installation directly into your environment. View on GitHub MCP Registry Configuration Precedence # Serena uses a cascading configuration system, allowing for both global preferences and project-specific overrides.\nPriority Level Location Purpose 1 (Highest) CLI Arguments Client config Overrides everything (e.g., --mode). 2 Project Config .serena/project.yml Per-project settings (e.g., read_only). 3 Global Config ~/.serena/serena_config.yml User preferences (e.g., max_answer_chars). 4 (Lowest) Defaults Source Code Hardcoded fallbacks. Comparative Analysis: Serena vs. Cursor # The market for AI coding tools is crowded, but Serena occupies a unique niche as a \u0026ldquo;Bring Your Own Model\u0026rdquo; (BYOM) middleware.\nFeature Serena MCP Cursor (Native) Understanding Symbolic (LSP). 100% deterministic. Hybrid (Vector + AST). Proprietary indexing. Architecture Middleware. Connects any LLM to any code. Monolith. The editor is the AI client. Cost Free / Open Source. Pay only for LLM API. Subscription. $20/month for Pro. Privacy High. Code stays local with local LLMs. Variable. Relies on Cursor\u0026rsquo;s cloud indexing. Verdict: Cursor is fantastic for a smooth \u0026ldquo;Copilot\u0026rdquo; experience. Serena is the superior choice for developers who want to orchestrate complex, cross-file tasks using models they control (including local ones).\nOperational Realities \u0026amp; Troubleshooting # Running an agentic server isn\u0026rsquo;t without its quirks. Here are a few things to keep in mind:\nThe \u0026ldquo;Zombie Process\u0026rdquo;: Sometimes, when you close your LLM client, the underlying language servers (like rust-analyzer) keep running. Check the Serena dashboard (default http://localhost:24282) to manually terminate sessions if your fans start spinning. Language Quirks: Go: Large monorepos can sometimes cause gopls to loop. Restricting project scope in project.yml usually fixes this. Java: The jdtls server has a slow startup. Pre-indexing with serena project index is highly recommended to avoid timeouts. Token Economy: Serena registers 20+ tools. This adds overhead to every prompt. For small projects, consider using \u0026ldquo;lite mode\u0026rdquo; or excluding unused tools to save on input tokens. Real-World Workflow: The Refactor # To see Serena in action, imagine renaming a core domain entity like Customer to Client.\nLocate: The agent calls find_symbol to find the class definition. Analyze: It calls find_referencing_symbols to build an impact map of every usage. Execute: It calls rename_symbol. Because it uses the LSP, it won\u0026rsquo;t accidentally rename a variable named customer_service or a string in a comment. Verify: It runs execute_shell_command to trigger the test suite. This deterministic loop is what makes Serena a true agent rather than just a chatty assistant.\nConclusion: The Rise of the Agentic IDE # Serena is a concrete step toward the \u0026ldquo;Agentic IDE\u0026rdquo;—a development environment where the LLM isn\u0026rsquo;t just a passive observer, but a collaborator that understands symbol boundaries, project conventions, and the deterministic reality of your code.\nWhile it introduces some operational overhead, the payoff is a level of reliability that probabilistic RAG simply can\u0026rsquo;t match. If you\u0026rsquo;re tired of \u0026ldquo;hallucinated refactors\u0026rdquo; and want an agent that actually knows its way around a codebase, Serena is well worth the setup time.\nReferences \u0026amp; Further Reading\nSerena GitHub Repository Language Server Protocol Official Docs Model Context Protocol (MCP) Specification Serena Github MCP Repository - One Click Install! ","date":"17 December 2025","externalUrl":null,"permalink":"/posts/serena-mcp-server/","section":"Posts","summary":"","title":"Serena MCP Server: Turning LLMs into Deterministic Code Agents","type":"posts"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"17 December 2025","externalUrl":null,"permalink":"/categories/tools/","section":"Categories","summary":"","title":"Tools","type":"categories"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/categories/architecture/","section":"Categories","summary":"","title":"Architecture","type":"categories"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/tags/architecture/","section":"Tags","summary":"","title":"Architecture","type":"tags"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/tags/azure/","section":"Tags","summary":"","title":"Azure","type":"tags"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/categories/development/","section":"Categories","summary":"","title":"Development","type":"categories"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/tags/litellm/","section":"Tags","summary":"","title":"LiteLLM","type":"tags"},{"content":"You\u0026rsquo;re juggling Ollama for local models, Azure AI Foundry for production, LM Studio for experimentation, and maybe OpenAI as a fallback. Each has its own SDK, authentication pattern, and quirks. Your codebase is littered with conditional logic: \u0026ldquo;If local, use this URL; if Azure, use that SDK; if OpenRouter, do something else entirely.\u0026rdquo;\nThis is the multi-provider chaos problem, and it\u0026rsquo;s exhausting.\nEnter LiteLLM—a unified API gateway that sits between your application and every AI provider you use. It speaks one language (the OpenAI API format) while routing requests to 100+ providers behind the scenes. Whether you\u0026rsquo;re hitting GPT-4 in Azure, Llama 3 in Ollama, or Mistral on your LM Studio instance, your code stays the same.\nIn this post, we\u0026rsquo;ll deploy LiteLLM locally using Docker Compose with a PostgreSQL database for observability and cost tracking. We\u0026rsquo;ll explore the problems it solves, the tradeoffs you\u0026rsquo;ll face, and how it compares to cloud alternatives like OpenRouter.\nThe Problem: Provider Fragmentation # Let\u0026rsquo;s say you\u0026rsquo;re building an AI-powered feature. Your requirements are:\nLocal development: Use Ollama models (free, fast, private). Production: Use Azure OpenAI for compliance and enterprise SLAs. Experimentation: Try new models from Anthropic, Cohere, or Hugging Face. Without a gateway, your code looks like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (environment == \u0026#34;local\u0026#34;) { // Ollama-specific setup var ollamaClient = new HttpClient { BaseAddress = new Uri(\u0026#34;http://localhost:11434\u0026#34;) }; } else if (environment == \u0026#34;azure\u0026#34;) { // Azure OpenAI SDK var azureClient = new OpenAIClient(new Uri(azureEndpoint), new AzureKeyCredential(apiKey)); } else if (environment == \u0026#34;anthropic\u0026#34;) { // Anthropic SDK with different request format var anthropicClient = new AnthropicClient(apiKey); } This is brittle. Every time you add a provider, you add conditional branches, new SDKs, and new failure modes.\nThe Solution: LiteLLM as a Unified Gateway # LiteLLM acts as a reverse proxy and translation layer. You send requests in the OpenAI format, and LiteLLM handles the provider-specific translation.\nArchitecture:\n1 Your .NET App → LiteLLM (http://localhost:4000) → Ollama, Azure, OpenAI, Anthropic, etc. Your application code becomes:\n1 2 3 4 5 6 7 8 9 // One client, one format, any provider var client = new OpenAIClient(new Uri(\u0026#34;http://localhost:4000/v1\u0026#34;), new AzureKeyCredential(\u0026#34;your-litellm-key\u0026#34;)); var response = await client.GetChatCompletionsAsync( deploymentOrModelName: \u0026#34;ollama/llama3\u0026#34;, // or \u0026#34;azure/gpt-4\u0026#34;, \u0026#34;openai/gpt-4o\u0026#34;, etc. new ChatCompletionsOptions { Messages = { new ChatRequestUserMessage(\u0026#34;Explain quantum computing.\u0026#34;) } }); Notice the model name: ollama/llama3. LiteLLM uses provider prefixes to route the request. Change ollama/llama3 to azure/gpt-4, and your code doesn\u0026rsquo;t change—LiteLLM handles everything.\nDeploying LiteLLM Locally with Docker Compose # Let\u0026rsquo;s set up a production-grade local deployment with:\nLiteLLM: The gateway. PostgreSQL: To store request logs, costs, and usage analytics. Persistent storage: So your data survives restarts. Step 1: Create the Docker Compose File # Create a file named docker-compose.yml:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 version: \u0026#39;3.8\u0026#39; services: # PostgreSQL database for LiteLLM\u0026#39;s observability and analytics postgres: image: postgres:16-alpine container_name: litellm-db environment: POSTGRES_DB: litellm POSTGRES_USER: litellm_user POSTGRES_PASSWORD: your_secure_password_here volumes: - litellm-db-data:/var/lib/postgresql/data ports: - \u0026#34;5432:5432\u0026#34; healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U litellm_user -d litellm\u0026#34;] interval: 10s timeout: 5s retries: 5 networks: - litellm-network # LiteLLM proxy server litellm: image: ghcr.io/berriai/litellm:main-latest container_name: litellm-proxy ports: - \u0026#34;4000:4000\u0026#34; environment: # Database connection DATABASE_URL: \u0026#34;postgresql://litellm_user:your_secure_password_here@postgres:5432/litellm\u0026#34; # Optional: Set a master key for API authentication LITELLM_MASTER_KEY: \u0026#34;sk-1234-your-master-key\u0026#34; # Optional: Enable detailed logging LITELLM_LOG: \u0026#34;INFO\u0026#34; # Store the config file path LITELLM_CONFIG_PATH: /app/config.yaml volumes: # Mount your LiteLLM configuration file - ./litellm-config.yaml:/app/config.yaml # Optional: Persist logs - ./logs:/app/logs depends_on: postgres: condition: service_healthy networks: - litellm-network restart: unless-stopped volumes: litellm-db-data: networks: litellm-network: driver: bridge Step 2: Create the LiteLLM Configuration File # Create litellm-config.yaml in the same directory:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 model_list: # Azure OpenAI models - model_name: azure/gpt-4 litellm_params: model: azure/gpt-4 api_base: https://your-resource.openai.azure.com api_key: os.environ/AZURE_API_KEY api_version: \u0026#34;2024-02-15-preview\u0026#34; - model_name: azure/gpt-35-turbo litellm_params: model: azure/gpt-35-turbo api_base: https://your-resource.openai.azure.com api_key: os.environ/AZURE_API_KEY api_version: \u0026#34;2024-02-15-preview\u0026#34; # Ollama local models - model_name: ollama/llama3 litellm_params: model: ollama/llama3 api_base: http://host.docker.internal:11434 # Access host\u0026#39;s Ollama from Docker - model_name: ollama/mistral litellm_params: model: ollama/mistral api_base: http://host.docker.internal:11434 # LM Studio models (if running locally) - model_name: lmstudio/local-model litellm_params: model: openai/local-model # LM Studio uses OpenAI-compatible API api_base: http://host.docker.internal:1234/v1 # OpenAI direct (optional) - model_name: openai/gpt-4o litellm_params: model: gpt-4o api_key: os.environ/OPENAI_API_KEY # Optional: Set up cost tracking and budgets litellm_settings: # Enable request/response logging to the database success_callback: [\u0026#34;langfuse\u0026#34;] # Set budget alerts (in USD) max_budget: 100 budget_duration: 30d # 30 day rolling window # Optional: Set up user/team management general_settings: master_key: \u0026#34;sk-1234-your-master-key\u0026#34; database_url: \u0026#34;postgresql://litellm_user:your_secure_password_here@postgres:5432/litellm\u0026#34; Step 3: Set Environment Variables # Create a .env file for sensitive credentials (add this to .gitignore):\n1 2 3 AZURE_API_KEY=your_azure_openai_key OPENAI_API_KEY=your_openai_key ANTHROPIC_API_KEY=your_anthropic_key Update your docker-compose.yml to load the .env file:\n1 2 3 4 litellm: # ... existing config ... env_file: - .env Step 4: Start the Stack # 1 docker-compose up -d LiteLLM will start on http://localhost:4000. The database will be initialized automatically.\nCheck the logs:\n1 docker-compose logs -f litellm You should see:\n1 INFO: LiteLLM Proxy running on http://0.0.0.0:4000 Step 5: Test It # Use curl to send a request:\n1 2 3 4 5 6 7 curl http://localhost:4000/v1/chat/completions \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -H \u0026#34;Authorization: Bearer sk-1234-your-master-key\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;ollama/llama3\u0026#34;, \u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;What is LiteLLM?\u0026#34;}] }\u0026#39; If you have Ollama running locally with the llama3 model, you\u0026rsquo;ll get a response routed through LiteLLM.\nUsing LiteLLM from .NET # Here\u0026rsquo;s how to integrate it into your .NET application:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using Azure.AI.OpenAI; using Azure; // Configure the OpenAI client to point at LiteLLM var client = new OpenAIClient( new Uri(\u0026#34;http://localhost:4000/v1\u0026#34;), new AzureKeyCredential(\u0026#34;sk-1234-your-master-key\u0026#34;)); // Use any model configured in LiteLLM var response = await client.GetChatCompletionsAsync( deploymentOrModelName: \u0026#34;ollama/llama3\u0026#34;, // Switch to \u0026#34;azure/gpt-4\u0026#34; for production new ChatCompletionsOptions { Messages = { new ChatRequestSystemMessage(\u0026#34;You are a helpful AI assistant.\u0026#34;), new ChatRequestUserMessage(\u0026#34;Explain Docker Compose in one sentence.\u0026#34;) }, Temperature = 0.7f, MaxTokens = 150 }); Console.WriteLine(response.Value.Choices[0].Message.Content); To switch providers, just change the model name. No SDK changes, no conditional logic.\nObservability and Cost Tracking # One of LiteLLM\u0026rsquo;s best features is built-in observability. Every request is logged to the PostgreSQL database.\nView your costs and usage:\nLiteLLM includes a web UI. Access it at:\n1 http://localhost:4000/ui You\u0026rsquo;ll see:\nTotal requests per model Token usage (input/output) Cost estimates (based on provider pricing) Latency metrics This is invaluable for:\nDebugging which models are being called Identifying expensive queries Optimizing prompt lengths You can also query the database directly:\n1 2 3 4 5 6 7 -- Connect to the database docker exec -it litellm-db psql -U litellm_user -d litellm -- Query request logs SELECT model, COUNT(*), SUM(total_cost) FROM request_logs GROUP BY model; The Tradeoffs # LiteLLM is powerful, but it\u0026rsquo;s not a silver bullet. Here are the considerations:\nPros: # Unified Interface: One API for all providers. Massively simplifies your code. Cost Visibility: Track spending across providers in a single dashboard. Self-Hosted: Your data stays local. No third-party tracking. Fallback \u0026amp; Load Balancing: Configure fallback models if your primary fails (e.g., Ollama → Azure). Observability: Request logs, latency metrics, and error rates baked in. Cons: # Single Point of Failure: If LiteLLM goes down, all your AI calls fail. (Mitigate with health checks and restarts.) Latency Overhead: Adds ~10-50ms per request (negligible for most use cases). Configuration Complexity: The model_list YAML can get large if you support many models. No Built-in Caching: Unlike some gateways, LiteLLM doesn\u0026rsquo;t cache responses by default (you\u0026rsquo;ll need Redis or a custom solution). LiteLLM vs. OpenRouter vs. Other Gateways # Let\u0026rsquo;s compare LiteLLM to its alternatives:\nOpenRouter # What It Is: A cloud-hosted AI gateway (openrouter.ai). Pros: Zero setup. Just sign up and get an API key. Built-in credit system and cost tracking. Access to 100+ models from OpenAI, Anthropic, Google, open-source, and more. Cons: Cloud-only. Your requests go through OpenRouter\u0026rsquo;s servers (privacy concern for sensitive data). Cost markup: OpenRouter adds a small fee on top of provider costs. No local model support: Can\u0026rsquo;t route to your Ollama or LM Studio instances. When to Use: Quick prototypes, non-sensitive data, or when you want zero DevOps overhead. LiteLLM (Self-Hosted) # Pros: Full control. Your data never leaves your network. Supports local models (Ollama, LM Studio). Free (except infrastructure costs). Cons: Requires Docker and basic DevOps knowledge. You manage uptime, updates, and backups. When to Use: Privacy-sensitive applications, hybrid local/cloud workflows, or enterprise environments. Portkey.ai # What It Is: A cloud AI gateway with advanced features (caching, prompt management, A/B testing). Pros: Enterprise-grade features like semantic caching, prompt versioning, and analytics. Cons: Cloud-only, paid plans only for advanced features. AI Gateway (Cloudflare) # What It Is: Cloudflare\u0026rsquo;s free AI gateway for rate limiting and caching. Pros: Free, built into Cloudflare\u0026rsquo;s edge network (low latency). Cons: Limited to Cloudflare-hosted models and OpenAI/Anthropic proxying. Summary # Feature LiteLLM (Self-Hosted) OpenRouter Portkey.ai Cloudflare AI Gateway Self-Hosted ✅ ❌ ❌ ❌ Local Models ✅ ❌ ❌ ❌ Cost Tracking ✅ ✅ ✅ ✅ Built-in Caching ❌ ❌ ✅ ✅ Prompt Versioning ❌ ❌ ✅ ❌ Zero DevOps ❌ ✅ ✅ ✅ The verdict: Use LiteLLM if you need to consolidate local and cloud models with full data control. Use OpenRouter if you want a cloud solution with zero setup.\nHow LiteLLM Fits into a Self-Hosted AI Developer Workflow # Here\u0026rsquo;s a typical workflow for a developer using LiteLLM:\nLocal Development:\nUse Ollama models (ollama/llama3) for rapid iteration. Free, fast, private. LiteLLM routes all requests to http://localhost:11434. Staging:\nSwitch to a mid-tier cloud model (azure/gpt-35-turbo) for more accurate testing. LiteLLM routes to your Azure OpenAI instance. Production:\nDeploy with a premium model (azure/gpt-4, openai/gpt-4o). LiteLLM tracks costs and usage in the database. Experimentation:\nEasily test new providers (Anthropic Claude, Google Gemini) by adding them to litellm-config.yaml. No code changes required. The beauty: Your application code never changes. You swap models by updating a YAML file.\nPractical Use Cases for LiteLLM # Here are some real-world scenarios where LiteLLM shines:\n1. Multi-Tenant SaaS with Per-Customer Models # Your SaaS app lets customers choose their AI provider (for compliance or cost reasons). Customer A wants Azure, Customer B wants AWS Bedrock, Customer C wants local Ollama.\nSolution: Use LiteLLM with virtual keys. Each customer gets a key tied to a specific model in the config.\n2. Cost Optimization with Fallbacks # You want to use a cheap local model (ollama/phi-3) for simple queries and fall back to azure/gpt-4 for complex ones.\nSolution: Configure LiteLLM\u0026rsquo;s fallback logic:\n1 2 3 4 5 model_list: - model_name: smart-model litellm_params: model: ollama/phi-3 fallbacks: [\u0026#34;azure/gpt-4\u0026#34;] 3. Development/Production Parity # Devs use Ollama locally; production uses Azure. With LiteLLM, both environments use the same code—just different config files.\n4. Prompt Experimentation Across Providers # You\u0026rsquo;re A/B testing whether GPT-4 or Claude 3.5 Sonnet performs better for summarization. Route 50% of traffic to each via LiteLLM\u0026rsquo;s load balancing.\nAdvanced Tips # Enable Caching for Repeated Queries # LiteLLM doesn\u0026rsquo;t include caching, but you can add Redis:\n1 2 3 4 5 6 7 # In docker-compose.yml redis: image: redis:7-alpine ports: - \u0026#34;6379:6379\u0026#34; # Update your .NET app to check Redis before calling LiteLLM Monitor with Prometheus # LiteLLM exposes Prometheus metrics at http://localhost:4000/metrics. Scrape these for uptime and latency monitoring.\nUse Virtual Keys for Team Management # Create API keys for different teams or projects, each with its own budget:\n1 2 3 curl http://localhost:4000/key/generate \\ -H \u0026#34;Authorization: Bearer sk-1234-your-master-key\u0026#34; \\ -d \u0026#39;{\u0026#34;team_id\u0026#34;: \u0026#34;engineering\u0026#34;, \u0026#34;max_budget\u0026#34;: 50}\u0026#39; Getting Started Checklist # Install Docker and Docker Compose Create docker-compose.yml and litellm-config.yaml Add your provider API keys to a .env file Run docker-compose up -d Test with curl or your .NET app Access the UI at http://localhost:4000/ui to monitor usage Conclusion # LiteLLM is a game-changer for developers juggling multiple AI providers. It unifies Ollama, Azure AI Foundry, OpenAI, Anthropic, and dozens of others behind a single, OpenAI-compatible API. With Docker Compose and PostgreSQL, you get observability, cost tracking, and full data control—all running on your own hardware.\nIf you\u0026rsquo;re tired of maintaining provider-specific SDKs and conditionals, give LiteLLM a try. Your codebase (and your sanity) will thank you.\nFurther Reading # LiteLLM Official Documentation - Comprehensive guides and API reference LiteLLM GitHub Repository - Source code and issue tracker LiteLLM Proxy Server Docs - Detailed proxy setup guide Supported Providers - Complete list of 100+ supported AI providers OpenRouter Documentation - Alternative cloud gateway Ollama Documentation - Running local models Azure AI Foundry - Microsoft\u0026rsquo;s AI platform LiteLLM Docker Images - Official Docker images LiteLLM Cost Tracking Tutorial - Setting up budgets and alerts Langfuse Integration - Advanced observability with Langfuse ","date":"9 December 2025","externalUrl":null,"permalink":"/posts/litellm-unified-ai-gateway-self-hosted/","section":"Posts","summary":"","title":"LiteLLM: Your Self-Hosted AI Gateway for Local and Cloud Models","type":"posts"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/tags/ollama/","section":"Tags","summary":"","title":"Ollama","type":"tags"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/tags/openai/","section":"Tags","summary":"","title":"OpenAI","type":"tags"},{"content":"","date":"9 December 2025","externalUrl":null,"permalink":"/tags/self-hosted/","section":"Tags","summary":"","title":"Self-Hosted","type":"tags"},{"content":"We are living through a gold rush. The barriers to creating software have never been lower, and the capabilities of our tools have never been higher. With a few keystrokes, we can generate entire applications, analyze massive datasets, and deploy autonomous agents.\nBut as the old adage goes, with great power comes great responsibility. In the age of AI, the role of the software developer is evolving from \u0026ldquo;builder\u0026rdquo; to \u0026ldquo;guardian.\u0026rdquo; We are no longer just responsible for whether the code compiles; we are responsible for the impact that code has on the world.\nThe \u0026ldquo;Black Box\u0026rdquo; Problem # The cardinal rule of responsible AI development is simple: Never ship code you do not understand.\nIt is incredibly tempting to let an AI assistant generate a complex regular expression or a cryptographic implementation, paste it into your IDE, and commit it because \u0026ldquo;it works.\u0026rdquo; This is dangerous. AI models are prone to hallucinations and subtle bugs. They can introduce security vulnerabilities that are invisible to the untrained eye.\nWhen you commit code, you are the signatory. You are vouching for its correctness, its security, and its maintainability. If that code causes a data breach or a production outage, \u0026ldquo;the AI wrote it\u0026rdquo; is not a valid defense. You must treat AI-generated code with more skepticism than code written by a human peer, not less. Read it, test it, and understand it before you ship it.\nThe Guardian of Data # Data is the fuel of the AI revolution, but it is also the most sensitive asset we manage. As developers, we are the gatekeepers of user trust.\nWe must be hyper-vigilant about what data we send to third-party AI models. Pasting a customer\u0026rsquo;s database schema or a snippet of code containing API keys into a public chatbot is a massive security risk. That data may be used to train future versions of the model, potentially leaking your trade secrets or user data to the world.\nResponsible development means implementing strict data sanitization pipelines. It means using local models or enterprise-grade instances with data privacy guarantees when dealing with PII (Personally Identifiable Information). It means fighting for the user\u0026rsquo;s privacy even when it\u0026rsquo;s inconvenient.\nBias and Fairness # AI models are mirrors. They reflect the data they were trained on, and that data contains the biases, prejudices, and blind spots of the internet. If we are not careful, we will build applications that amplify these biases at scale.\nWe have already seen examples of AI recruiting tools that penalize women, facial recognition systems that fail on darker skin tones, and lending algorithms that discriminate against minorities. As developers, we cannot wash our hands of this. We must actively test our systems for bias. We must ask: Who is this working for? And who is it failing?\nThis requires a diversity of thought in the development team. It requires us to look beyond the \u0026ldquo;happy path\u0026rdquo; and consider the edge cases where algorithmic cruelty can occur.\nThe Environmental Cost # Finally, we must consider the environmental impact of our tools. Training a large language model consumes a staggering amount of energy and water. Running inference on every user request adds up.\nResponsible development means using the right tool for the job. Do you really need a GPT-4 class model to parse a date string? Or could you write a simple function? Do you need to generate a new image for every user, or can you cache it?\nWe should treat compute as a finite resource, not an infinite one. Efficiency is not just about cost; it\u0026rsquo;s about sustainability.\nThe Human Element # AI can generate code, but it cannot generate empathy. It cannot make ethical judgment calls. It cannot stand up to a product manager and say, \u0026ldquo;We shouldn\u0026rsquo;t build this feature because it exploits our users.\u0026rdquo;\nThat is your job. In an age of automation, your humanity is your most valuable asset. Be the developer who codes with a conscience.\n","date":"8 December 2025","externalUrl":null,"permalink":"/posts/responsible-developer-age-of-ai/","section":"Posts","summary":"","title":"Coding with Conscience: Responsibility in the AI Era","type":"posts"},{"content":"","date":"8 December 2025","externalUrl":null,"permalink":"/categories/ethics/","section":"Categories","summary":"","title":"Ethics","type":"categories"},{"content":"","date":"8 December 2025","externalUrl":null,"permalink":"/tags/ethics/","section":"Tags","summary":"","title":"Ethics","type":"tags"},{"content":"","date":"8 December 2025","externalUrl":null,"permalink":"/tags/privacy/","section":"Tags","summary":"","title":"Privacy","type":"tags"},{"content":"","date":"8 December 2025","externalUrl":null,"permalink":"/tags/responsibility/","section":"Tags","summary":"","title":"Responsibility","type":"tags"},{"content":"","date":"8 December 2025","externalUrl":null,"permalink":"/categories/technology/","section":"Categories","summary":"","title":"Technology","type":"categories"},{"content":"","date":"7 December 2025","externalUrl":null,"permalink":"/categories/career/","section":"Categories","summary":"","title":"Career","type":"categories"},{"content":"","date":"7 December 2025","externalUrl":null,"permalink":"/tags/career/","section":"Tags","summary":"","title":"Career","type":"tags"},{"content":"","date":"7 December 2025","externalUrl":null,"permalink":"/tags/future-of-work/","section":"Tags","summary":"","title":"Future of Work","type":"tags"},{"content":"","date":"7 December 2025","externalUrl":null,"permalink":"/tags/productivity/","section":"Tags","summary":"","title":"Productivity","type":"tags"},{"content":"There is a palpable anxiety in the developer community right now. Every week, a new AI model is released that claims to outperform human programmers. We see demos of agents building entire websites from a napkin sketch, and we wonder: Is this the end of coding?\nThe short answer is no. But the long answer is that \u0026ldquo;coding\u0026rdquo; as we know it is changing fundamentally, and ignoring this shift is a career risk you cannot afford to take. The developers who thrive in the next decade won\u0026rsquo;t be the ones who fight AI; they will be the ones who master it.\nThe Force Multiplier # The most helpful way to view AI is not as a replacement, but as a force multiplier. Throughout the history of computing, we have constantly sought to automate the tedious parts of our job so we can focus on the creative parts.\nWe moved from punch cards to Assembly, from Assembly to C, and from C to high-level languages like Python and C#. Each step abstracted away the \u0026ldquo;how\u0026rdquo; so we could focus on the \u0026ldquo;what.\u0026rdquo; AI is simply the next logical step in this evolution. It is the ultimate abstraction layer.\nWhen you use an AI assistant, you are no longer limited by your typing speed or your ability to memorize syntax. You are limited only by your ability to clearly articulate a problem and design a solution. A single developer, armed with AI tools, can now do the work that used to require a team of three. They can scaffold projects in minutes, write unit tests in seconds, and debug obscure errors instantly.\nIf you choose to ignore these tools, you aren\u0026rsquo;t just \u0026ldquo;doing it the old way\u0026rdquo;—you are voluntarily choosing to be slower and less effective than your peers.\nFrom Syntax to Semantics # For decades, the barrier to entry in software development was syntax. You had to know where the semicolons went, how to manage memory, and the specific incantations to make a compiler happy.\nAI is lowering that barrier to near zero. But this doesn\u0026rsquo;t mean the job is disappearing; it means the job is moving up the stack. The value of a developer is shifting from writing code to designing systems.\nIn an AI-driven world, your ability to write a for loop is irrelevant. What matters is your ability to:\nUnderstand system architecture and how components fit together. Identify security vulnerabilities and performance bottlenecks. Translate vague business requirements into technical specifications. Verify and validate the output of the AI. We are transitioning from being \u0026ldquo;writers\u0026rdquo; of code to being \u0026ldquo;editors\u0026rdquo; and \u0026ldquo;architects\u0026rdquo; of code. The skill set is changing from memorization to critical thinking.\n\u0026ldquo;Prompting\u0026rdquo; is the New \u0026ldquo;Googling\u0026rdquo; # Remember when \u0026ldquo;Googling\u0026rdquo; became a legitimate job skill? Knowing how to construct a search query to find the right StackOverflow answer became a hallmark of a senior developer.\nToday, Prompt Engineering is that skill. It is the art of communicating intent to a non-human intelligence. It requires precision, context, and an understanding of how LLMs \u0026ldquo;think.\u0026rdquo;\nDevelopers who dismiss prompting as a fad are missing the point. Learning how to effectively context-load a model, how to use chain-of-thought reasoning, and how to iterate on a prompt to get the desired output is the new literacy. It is the interface through which we will control the most powerful computing resources ever built.\nDon\u0026rsquo;t Drown in the Hype # Keeping up with AI doesn\u0026rsquo;t mean you need to read every research paper or try every new tool that launches on Product Hunt. That is a recipe for burnout.\nInstead, focus on the principles. Understand what a Large Language Model is and what its limitations are. Understand the concepts of Retrieval Augmented Generation (RAG) and Agents. Understand the difference between a probabilistic answer and a deterministic one.\nOnce you understand the underlying mechanics, the specific tools become interchangeable. Whether you use GitHub Copilot, ChatGPT, or a local Llama model is less important than understanding how to apply them to solve real problems.\nThe wave is here. You can try to hold back the ocean, or you can grab a surfboard. The choice is yours.\nFurther Reading # Microsoft AI for Developers - Microsoft\u0026rsquo;s AI learning hub OpenAI Documentation - Understanding LLMs and APIs Hugging Face - Community hub for AI models and datasets Prompt Engineering Guide - Comprehensive guide to prompt engineering ","date":"7 December 2025","externalUrl":null,"permalink":"/posts/keeping-up-with-ai-trends/","section":"Posts","summary":"","title":"Riding the Wave: Why Developers Must Embrace AI","type":"posts"},{"content":"","date":"6 December 2025","externalUrl":null,"permalink":"/tags/leadership/","section":"Tags","summary":"","title":"Leadership","type":"tags"},{"content":"","date":"6 December 2025","externalUrl":null,"permalink":"/tags/mentorship/","section":"Tags","summary":"","title":"Mentorship","type":"tags"},{"content":"","date":"6 December 2025","externalUrl":null,"permalink":"/tags/soft-skills/","section":"Tags","summary":"","title":"Soft Skills","type":"tags"},{"content":"In the fast-paced world of software development, we often measure our worth by the lines of code we ship, the complexity of the systems we architect, or the speed at which we close tickets. We obsess over clean architecture, test coverage, and performance metrics. But after years in the industry, I’ve come to a realization that often surprises new engineers: code is temporary, but influence is permanent.\nThe systems you build today will likely be rewritten, refactored, or deprecated within five years. The frameworks you master will become obsolete. But the people you mentor? The junior engineers you help navigate their first production outage? That impact lasts a lifetime.\nMentorship is often viewed through a narrow lens: a senior developer teaching a junior developer how to write better code. While technical guidance is part of it, true mentorship is far more profound. It is a symbiotic relationship that shapes the culture of a team and the trajectory of careers.\nThe Senior Trap: \u0026ldquo;I Must Know Everything\u0026rdquo; # One of the biggest barriers to effective mentorship is the \u0026ldquo;Senior Trap\u0026rdquo;—the belief that to be a mentor, you must be an infallible oracle of technical knowledge. This creates a dynamic where the mentor feels pressure to have an immediate answer for every question, and the mentee feels afraid to ask \u0026ldquo;stupid\u0026rdquo; questions.\nThe best mentors I’ve ever had weren’t the ones who instantly knew the answer. They were the ones who said, \u0026ldquo;I have no idea why that’s happening. Let’s figure it out together.\u0026rdquo;\nThere is immense power in a senior engineer admitting ignorance. It validates the junior engineer’s struggle and transforms the interaction from a lecture into a collaborative investigation. It teaches the most valuable skill of all: not what the answer is, but how to find it. It models the resilience and curiosity required to survive in an industry where the tools change every six months.\nReverse Mentorship: Learning from the \u0026ldquo;New\u0026rdquo; # We often assume knowledge flows downhill, from senior to junior. This is a mistake. Junior engineers enter the field with a \u0026ldquo;beginner’s mind\u0026rdquo; that is incredibly valuable. They haven\u0026rsquo;t yet been jaded by \u0026ldquo;the way we\u0026rsquo;ve always done things.\u0026rdquo; They ask \u0026ldquo;why\u0026rdquo; when seniors have stopped asking.\nI have learned just as much from the people I’ve mentored as they have learned from me. They bring fresh perspectives on new tools, they challenge architectural dogma that I’ve accepted as fact, and their questions often expose gaps in my own understanding. If you can’t explain a complex concept simply to a junior engineer, you probably don’t understand it as well as you think you do.\nBeyond Syntax: Teaching the \u0026ldquo;Why\u0026rdquo; # If your mentorship sessions are strictly code reviews, you are missing the point. Syntax is easy; judgment is hard.\nThe role of a mentor is to help engineers move up the abstraction ladder. Instead of just pointing out a bug, discuss the design pattern that led to it. Instead of just optimizing a query, talk about the trade-offs between read-heavy and write-heavy systems.\nYou are teaching them how to think. You are teaching them how to communicate technical risks to non-technical stakeholders. You are teaching them when to push back on a requirement and when to disagree and commit. These \u0026ldquo;soft\u0026rdquo; skills are actually the hardest to acquire and the most critical for career growth.\nThe Only Legacy That Matters # Ten years from now, nobody will remember that you optimized that API response time by 50 milliseconds. But the engineer who was on the verge of burnout, whom you helped find balance? They will remember. The self-taught developer whom you encouraged to apply for a promotion? They will remember.\nAs we navigate our careers, we must remember that our primary job isn\u0026rsquo;t just to build software; it\u0026rsquo;s to build the builders of software. By investing in mentorship, we ensure that the values of craftsmanship, empathy, and curiosity survive long after our git commits have been buried in history.\n","date":"6 December 2025","externalUrl":null,"permalink":"/posts/mentorship-software-developer/","section":"Posts","summary":"","title":"The Two-Way Street of Software Mentorship","type":"posts"},{"content":"","date":"5 December 2025","externalUrl":null,"permalink":"/tags/.net/","section":"Tags","summary":"","title":".NET","type":"tags"},{"content":"The biggest limitation of LLMs isn\u0026rsquo;t their intelligence; it\u0026rsquo;s their memory. They know everything about the world up to 2023, but they know nothing about your company\u0026rsquo;s internal error codes, your new product manual, or your project documentation.\nRetrieval Augmented Generation (RAG) is the architecture that solves this. It allows you to \u0026ldquo;inject\u0026rdquo; relevant knowledge into the AI\u0026rsquo;s prompt before it answers.\nIn this post, we\u0026rsquo;ll use Kernel Memory—an open-source service from Microsoft—to build a Troubleshooting Bot that knows how to fix your specific application errors.\nThe \u0026ldquo;Memory\u0026rdquo; Problem # If you ask ChatGPT \u0026ldquo;How do I fix Error Code 99 in Project Omega?\u0026rdquo;, it will hallucinate a plausible-sounding but incorrect answer. It doesn\u0026rsquo;t know what \u0026ldquo;Project Omega\u0026rdquo; is.\nWith RAG, the flow changes:\nUser: \u0026ldquo;How do I fix Error Code 99?\u0026rdquo; System: Searches your database for \u0026ldquo;Error Code 99\u0026rdquo;. System: Finds a document: \u0026ldquo;Error 99: Flux Capacitor misalignment. Fix: Rotate 90 degrees.\u0026rdquo; System: Sends the user\u0026rsquo;s question plus that document to the AI. AI: \u0026ldquo;To fix Error Code 99, you need to rotate the Flux Capacitor 90 degrees.\u0026rdquo; Building the Bot # We\u0026rsquo;ll use the Microsoft.KernelMemory.Core package. It handles the entire pipeline: reading files, splitting them into chunks, generating embeddings (vectors), storing them, and searching them.\n1. Setup # Prerequisites: We\u0026rsquo;ll use the \u0026ldquo;batteries included\u0026rdquo; package which allows running Kernel Memory in a serverless (embedded) mode without setting up a separate web service.\n1 dotnet add package Microsoft.KernelMemory 2. The Code # We\u0026rsquo;ll simulate a scenario where we have a markdown file containing internal troubleshooting steps. We\u0026rsquo;ll ingest this text and then ask the AI about it.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 using Microsoft.KernelMemory; // 1. Initialize Kernel Memory // In production, you\u0026#39;d point this to Azure AI Search or Qdrant. // For this demo, we use the \u0026#34;Serverless\u0026#34; mode which runs entirely in memory // but still needs an LLM for embeddings and generation (OpenAI here). var memory = new KernelMemoryBuilder() .WithOpenAIDefaults(Environment.GetEnvironmentVariable(\u0026#34;OPENAI_API_KEY\u0026#34;)) .Build(); // 2. The Knowledge Base // Imagine this content came from a \u0026#39;troubleshooting.md\u0026#39; file or a Wiki. var troubleshootingGuide = @\u0026#34; # Project Omega Troubleshooting Guide ## Error Code 101: Connection Refused Cause: The firewall is blocking port 8080. Fix: Run `netsh advfirewall set allprofiles state off` (Not recommended for prod) or add an allow rule for TCP 8080. ## Error Code 202: Out of Coffee Cause: The developer is tired. Fix: Refill the pot immediately. Critical priority. ## Error Code 303: Quantum Entanglement Cause: You observed the electron. Fix: Stop looking at it. \u0026#34;; Console.WriteLine(\u0026#34;Ingesting knowledge base...\u0026#34;); // 3. Ingest Data // This splits the text into paragraphs, converts them to vectors, and stores them. await memory.ImportTextAsync(troubleshootingGuide, documentId: \u0026#34;guide-v1\u0026#34;); // 4. Ask Questions Console.WriteLine(\u0026#34;Ready! Ask a question about error codes.\u0026#34;); var question = \u0026#34;How do I fix the quantum error?\u0026#34;; Console.WriteLine($\u0026#34;\\nQuestion: {question}\u0026#34;); var answer = await memory.AskAsync(question); Console.WriteLine($\u0026#34;\\nAnswer: {answer.Result}\u0026#34;); // 5. Cite Sources // RAG allows you to show *where* the answer came from. Console.WriteLine(\u0026#34;\\n--- Sources ---\u0026#34;); foreach (var source in answer.RelevantSources) { Console.WriteLine($\u0026#34;Source: {source.SourceName} (Relevance: {source.Partitions.First().Relevance:P1})\u0026#34;); } Expected Output # 1 2 3 4 5 6 Question: How do I fix the quantum error? Answer: To fix the quantum error (Error Code 303), you simply need to stop looking at the electron. --- Sources --- Source: guide-v1 (Relevance: 89.5%) Under the Hood: Embeddings \u0026amp; Vector DBs # Kernel Memory abstracts away the complexity, but it\u0026rsquo;s important to know what\u0026rsquo;s happening:\nChunking: The text is split into smaller pieces (e.g., paragraphs). If we didn\u0026rsquo;t do this, the whole document might not fit in the LLM\u0026rsquo;s context window. Embeddings: Each chunk is sent to an Embedding Model (like text-embedding-3-small). This model turns text into a list of numbers (a vector) representing its meaning. Vector Search: When you ask a question, your question is also turned into a vector. The system finds chunks that are mathematically \u0026ldquo;close\u0026rdquo; to your question\u0026rsquo;s vector. Going to Production # For a real app, you wouldn\u0026rsquo;t use the in-memory storage (which is lost when the app restarts). You would configure a persistent Vector Database like Qdrant, Azure AI Search, or Postgres.\nYou\u0026rsquo;ll need to install the specific adapter package, e.g., Microsoft.KernelMemory.MemoryDb.Qdrant.\n1 2 3 4 var memory = new KernelMemoryBuilder() .WithOpenAIDefaults(apiKey) .WithQdrantMemoryDb(\u0026#34;http://localhost:6333\u0026#34;) // Requires Qdrant adapter package .Build(); This allows you to ingest gigabytes of documents and search them in milliseconds.\nFurther Reading # Kernel Memory on GitHub - Official repository with extensive documentation Qdrant Vector Database - Popular vector database documentation Azure AI Search - Microsoft\u0026rsquo;s managed vector search service RAG Pattern Overview - Azure Architecture Center guide ","date":"5 December 2025","externalUrl":null,"permalink":"/posts/rag-pipelines-kernel-memory/","section":"Posts","summary":"","title":"Building RAG Pipelines with Kernel Memory","type":"posts"},{"content":"","date":"5 December 2025","externalUrl":null,"permalink":"/tags/c%23/","section":"Tags","summary":"","title":"C#","type":"tags"},{"content":"","date":"5 December 2025","externalUrl":null,"permalink":"/tags/kernel-memory/","section":"Tags","summary":"","title":"Kernel Memory","type":"tags"},{"content":"","date":"5 December 2025","externalUrl":null,"permalink":"/tags/knowledge-management/","section":"Tags","summary":"","title":"Knowledge Management","type":"tags"},{"content":"","date":"5 December 2025","externalUrl":null,"permalink":"/tags/rag/","section":"Tags","summary":"","title":"RAG","type":"tags"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/local-llm/","section":"Tags","summary":"","title":"Local LLM","type":"tags"},{"content":"Cloud-based AI models like GPT-4 are powerful, but they come with trade-offs: latency, cost, and privacy. If you\u0026rsquo;re analyzing sensitive server logs or PII, sending that data to the cloud might be a non-starter.\nIn this post, we\u0026rsquo;ll build a Local Log Analyzer that runs entirely on your machine using Ollama and .NET.\nWhy Local AI? # Privacy: Your data never leaves your network. Cost: Zero API fees, no matter how many tokens you process. Latency: No network round-trips; speed depends entirely on your hardware. Offline: Works on an air-gapped server or a plane. The Stack # Ollama: A lightweight tool to run models like Llama 3, Phi-3, or Mistral locally. Semantic Kernel: The .NET SDK to orchestrate the interaction. Llama 3 (8B): A powerful, efficient model that runs well on most modern laptops (requires ~8GB RAM). Step 1: Setup Ollama # Download Ollama from ollama.com. Once installed, pull the model we\u0026rsquo;ll use:\n1 ollama pull llama3 By default, Ollama starts a local API server at http://localhost:11434.\nStep 2: The Code (Log Analyzer) # We\u0026rsquo;ll write a C# program that reads a raw, messy error log and asks the local AI to extract the key details into a clean format.\nPrerequisites:\n1 dotnet add package Microsoft.SemanticKernel The Code:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; // 1. Configure the Kernel to talk to Ollama // Ollama provides an OpenAI-compatible API, so we use the standard OpenAI connector. var builder = Kernel.CreateBuilder(); builder.AddOpenAIChatCompletion( modelId: \u0026#34;llama3\u0026#34;, // The model name you pulled in Ollama apiKey: \u0026#34;ollama\u0026#34;, // Ollama ignores this, but the SDK requires a non-empty string endpoint: new Uri(\u0026#34;http://localhost:11434/v1\u0026#34;)); // The local API endpoint var kernel = builder.Build(); // 2. Define our messy input data var rawLogEntry = @\u0026#34; [2025-12-04 14:22:11] [ERROR] [AuthService] Connection timeout while reaching DB_USERS (192.168.1.55). Retry count: 3. Exception: System.Net.Sockets.SocketException: Host is down. \u0026#34;; // 3. Create the Prompt // We ask the model to act as a parser. var prompt = $@\u0026#34; You are a system log analyzer. Analyze the following log entry and extract the Timestamp, Service Name, Error Type, and Root Cause. Provide the output as a concise summary. Log Entry: {rawLogEntry} \u0026#34;; // 4. Run it locally Console.WriteLine(\u0026#34;Analyzing log locally...\u0026#34;); var result = await kernel.InvokePromptAsync(prompt); Console.WriteLine(\u0026#34;\\n--- Analysis Result ---\u0026#34;); Console.WriteLine(result); Expected Output # Because this runs locally, you\u0026rsquo;ll see the output appear almost instantly (depending on your GPU/CPU).\n1 2 3 4 5 --- Analysis Result --- Timestamp: 2025-12-04 14:22:11 Service: AuthService Error Type: Connection Timeout Root Cause: The host DB_USERS (192.168.1.55) is down (SocketException). Advanced Tips for Local Models # Context Window Management # Local models often have smaller context windows (e.g., 4k or 8k tokens) compared to cloud models (128k). If you\u0026rsquo;re analyzing huge log files, you\u0026rsquo;ll need to split them into chunks.\nTemperature Settings # For extraction tasks like this, you want the model to be precise, not creative. When creating your request, set the Temperature to 0.\n1 2 var settings = new OpenAIPromptExecutionSettings { Temperature = 0 }; var result = await kernel.InvokePromptAsync(prompt, new(settings)); Hardware Requirements # 7B/8B Models (Llama 3, Mistral): Require ~8GB RAM. Runs decent on CPU, fast on Apple Silicon/NVIDIA. Phi-3 (3.8B): Requires ~4GB RAM. Runs great on almost anything. 70B Models: Require ~48GB RAM. You\u0026rsquo;ll need a serious workstation or Mac Studio. Running AI locally puts you in full control. Whether for privacy compliance or just building cool tools that work offline, the combination of Ollama and .NET is incredibly potent.\nFurther Reading # Ollama Documentation - Official Ollama documentation Ollama Model Library - Browse available models Semantic Kernel Local Models - Using local models with SK Phi-3 Model Card - Details on Microsoft\u0026rsquo;s Phi-3 model ","date":"4 December 2025","externalUrl":null,"permalink":"/posts/local-ai-dotnet-ollama/","section":"Posts","summary":"","title":"Running Local AI with .NET and Ollama","type":"posts"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/.net-9/","section":"Tags","summary":"","title":".NET 9","type":"tags"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/categories/.net-maui/","section":"Categories","summary":"","title":".NET MAUI","type":"categories"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/caliburn.micro/","section":"Tags","summary":"","title":"Caliburn.Micro","type":"tags"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/maui/","section":"Tags","summary":"","title":"MAUI","type":"tags"},{"content":"If you\u0026rsquo;ve spent any significant time in the WPF trenches over the last decade, chances are you\u0026rsquo;ve crossed paths with Caliburn.Micro. For many of us, it was the framework that finally made MVVM \u0026ldquo;click.\u0026rdquo; It stripped away the boilerplate, replaced verbose configuration with smart conventions, and let us focus on building features.\nNow that .NET MAUI is the standard for cross-platform .NET development, you might be staring at your existing WPF codebases and wondering: \u0026ldquo;Do I have to learn a whole new MVVM framework? Do I have to give up my conventions?\u0026rdquo;\nThe answer is a resounding no.\nCaliburn.Micro is alive and well in the .NET MAUI era. While the underlying platform has changed, the \u0026ldquo;magic\u0026rdquo; feels remarkably familiar. In this post, we\u0026rsquo;ll walk through how to migrate the core structure of a Caliburn.Micro WPF app to .NET MAUI.\nThe Paradigm Shift: Goodbye Bootstrapper # In the WPF world, everything started with the Bootstrapper. You\u0026rsquo;d subclass BootstrapperBase, override OnStartup, and wire up your IoC container there. It was the heart of your application.\nIn .NET MAUI, the application lifecycle is different. We don\u0026rsquo;t have a Bootstrapper class anymore. Instead, the responsibilities of the bootstrapper have moved directly into the App class, which now inherits from Caliburn.Micro.Maui.MauiApplication.\nLet\u0026rsquo;s look at how to set this up from scratch.\nStep 1: The Setup # First, create a new .NET MAUI project. Once you\u0026rsquo;re in, you\u0026rsquo;ll need to install the package. Be careful here—you don\u0026rsquo;t just want the core library; you need the platform-specific integration.\n1 dotnet add package Caliburn.Micro.Maui Note: Ensure you are targeting .NET 9 or a compatible version, as Caliburn.Micro has kept pace with the latest platform updates.\nStep 2: The New \u0026ldquo;Bootstrapper\u0026rdquo; # Open your App.xaml.cs. In a standard MAUI app, this inherits from Application. We\u0026rsquo;re going to change that to Caliburn.Micro.Maui.MauiApplication.\nThis is where we configure our container and set the root view.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 using Caliburn.Micro; using Caliburn.Micro.Maui; using MyMauiApp.ViewModels; namespace MyMauiApp; public partial class App : Caliburn.Micro.Maui.MauiApplication { private SimpleContainer _container; public App() { InitializeComponent(); // 1. Initialize the framework Initialize(); // 2. Set the root view (Async!) DisplayRootViewForAsync\u0026lt;MainViewModel\u0026gt;(); } protected override void Configure() { _container = new SimpleContainer(); _container.Instance(_container); // 3. Register your services and ViewModels _container.Singleton\u0026lt;IWindowManager, WindowManager\u0026gt;(); _container.Singleton\u0026lt;IEventAggregator, EventAggregator\u0026gt;(); _container.PerRequest\u0026lt;MainViewModel\u0026gt;(); } protected override object GetInstance(Type service, string key) { return _container.GetInstance(service, key); } protected override IEnumerable\u0026lt;object\u0026gt; GetAllInstances(Type service) { return _container.GetAllInstances(service); } protected override void BuildUp(object instance) { _container.BuildUp(instance); } } Key Differences to Note: # Initialize() in Constructor: We call Initialize() directly in the constructor. Async Root View: We use DisplayRootViewForAsync\u0026lt;T\u0026gt;(). MAUI\u0026rsquo;s navigation primitives are asynchronous, and Caliburn respects that. Configure(): This looks almost identical to the WPF version. Your DI logic (whether you use SimpleContainer or Autofac) lives here. Step 3: The Entry Point # MAUI uses a builder pattern in MauiProgram.cs to construct the app. We need to make sure our App class is registered correctly.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 namespace MyMauiApp; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp\u0026lt;App\u0026gt;() // This points to our Caliburn-enhanced App class .ConfigureFonts(fonts =\u0026gt; { fonts.AddFont(\u0026#34;OpenSans-Regular.ttf\u0026#34;, \u0026#34;OpenSansRegular\u0026#34;); fonts.AddFont(\u0026#34;OpenSans-Semibold.ttf\u0026#34;, \u0026#34;OpenSansSemibold\u0026#34;); }); return builder.Build(); } } Step 4: The Magic (Conventions) # This is why we\u0026rsquo;re here. Does x:Name still automagically bind to properties? Yes.\nLet\u0026rsquo;s create a simple MainViewModel:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using Caliburn.Micro; namespace MyMauiApp.ViewModels; public class MainViewModel : Screen { private string _message = \u0026#34;Hello from Caliburn.Micro in MAUI!\u0026#34;; public string Message { get =\u0026gt; _message; set =\u0026gt; Set(ref _message, value); } public void ClickMe() { Message = \u0026#34;You clicked the button! Magic is real.\u0026#34;; } } And the corresponding MainView.xaml (in the Views folder):\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;ContentPage xmlns=\u0026#34;http://schemas.microsoft.com/dotnet/2021/maui\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2009/xaml\u0026#34; x:Class=\u0026#34;MyMauiApp.Views.MainView\u0026#34;\u0026gt; \u0026lt;VerticalStackLayout Spacing=\u0026#34;20\u0026#34; Padding=\u0026#34;30\u0026#34;\u0026gt; \u0026lt;!-- Convention Binding: Binds to \u0026#39;Message\u0026#39; property --\u0026gt; \u0026lt;Label x:Name=\u0026#34;Message\u0026#34; FontSize=\u0026#34;Large\u0026#34; HorizontalOptions=\u0026#34;Center\u0026#34; /\u0026gt; \u0026lt;!-- Convention Binding: Binds to \u0026#39;ClickMe\u0026#39; method --\u0026gt; \u0026lt;Button x:Name=\u0026#34;ClickMe\u0026#34; Text=\u0026#34;Click Me\u0026#34; HorizontalOptions=\u0026#34;Center\u0026#34; /\u0026gt; \u0026lt;/VerticalStackLayout\u0026gt; \u0026lt;/ContentPage\u0026gt; Just like in WPF, Caliburn.Micro locates MainView based on the name MainViewModel, binds the Label to the Message property, and wires the Button to the ClickMe method. No Command boilerplate required.\nPitfalls \u0026amp; Gotchas # While the MVVM structure is preserved, the platform underneath has shifted.\n1. Async Navigation # In WPF, TryClose() was synchronous. In MAUI, navigation is inherently async. You\u0026rsquo;ll find yourself using await more often when coordinating screens.\n2. XAML Dialect # Caliburn handles the binding, but it doesn\u0026rsquo;t translate the controls.\nWPF StackPanel -\u0026gt; MAUI VerticalStackLayout / HorizontalStackLayout WPF TextBox -\u0026gt; MAUI Entry or Editor WPF TextBlock -\u0026gt; MAUI Label You will still need to rewrite your XAML, but your ViewModels can often be ported with minimal changes (mostly changing namespaces).\nConclusion # Migrating from WPF to MAUI is a significant undertaking, but sticking with Caliburn.Micro reduces the cognitive load immensely. You keep your architectural patterns, your testing strategies, and your muscle memory for conventions.\nIf you have a mature WPF application built on Caliburn, you don\u0026rsquo;t need to throw it all away. The framework has evolved with the times, proving once again that good abstractions are timeless.\nFor a complete working example, check out the official Setup.Maui sample in the Caliburn.Micro repository.\nHappy coding!\n","date":"4 December 2025","externalUrl":null,"permalink":"/posts/migrating-wpf-caliburn-micro-to-maui/","section":"Posts","summary":"","title":"Migrating WPF Caliburn.Micro Apps to .NET MAUI: Keeping the Magic Alive","type":"posts"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/migration/","section":"Tags","summary":"","title":"Migration","type":"tags"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/mvvm/","section":"Tags","summary":"","title":"MVVM","type":"tags"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/wpf/","section":"Tags","summary":"","title":"WPF","type":"tags"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/news/","section":"Tags","summary":"","title":"News","type":"tags"},{"content":"","date":"4 December 2025","externalUrl":null,"permalink":"/tags/software-engineering/","section":"Tags","summary":"","title":"Software Engineering","type":"tags"},{"content":"It’s that time of year again. With the release of .NET 10, we also get our hands on C# 14.\nAs a language, C# has been on a steady trajectory of reducing boilerplate and improving expressiveness. If C# 12 and 13 were about laying the groundwork for performance and collection literals, C# 14 feels like a \u0026ldquo;Quality of Life\u0026rdquo; release. It tackles some of the longest-standing requests from the community (looking at you, field keyword) and smooths out rough edges in the type system.\nLet\u0026rsquo;s dive into the features that will actually change how you write code day-to-day.\nThe field Keyword: Finally! # If you\u0026rsquo;ve written C# for any length of time, you\u0026rsquo;ve written this pattern a thousand times. You need a property with just a little bit of logic in the setter—maybe validation, maybe raising an INotifyPropertyChanged event. Suddenly, your concise auto-property explodes into a verbose backing field ritual.\nBefore C# 14:\n1 2 3 4 5 6 7 8 9 private string _name; public string Name { get =\u0026gt; _name; set { _name = value ?? throw new ArgumentNullException(nameof(value)); } } We all hated writing _name. It polluted the class scope and felt like unnecessary noise.\nWith C# 14:\n1 2 3 4 5 public string Name { get; set =\u0026gt; field = value ?? throw new ArgumentNullException(nameof(value)); } The field keyword gives us access to the compiler-synthesized backing field. It’s cleaner, it keeps the scope contained to the property, and it removes the need for manual field declarations.\nWarning: If you have existing variables named field in your scope, you might hit a breaking change. You can use @field or this.field to disambiguate, but honestly, it\u0026rsquo;s probably better to just rename your variable.\nExtension Members: Beyond Methods # Extension methods have been a staple of C# since LINQ (C# 3.0). They allowed us to add methods to types we didn\u0026rsquo;t own. But they had limits: you couldn\u0026rsquo;t add properties, and you couldn\u0026rsquo;t add static members.\nC# 14 introduces a new extension syntax that blows these limitations wide open.\nExtension Properties # You can now add properties to existing types. This is fantastic for adding computed helpers to third-party classes.\n1 2 3 4 5 6 7 8 9 10 11 12 13 public static class EnumerableExtensions { // New \u0026#39;extension\u0026#39; block syntax extension\u0026lt;T\u0026gt;(IEnumerable\u0026lt;T\u0026gt; source) { // Look ma, an extension property! public bool IsEmpty =\u0026gt; !source.Any(); } } // Usage var list = new List\u0026lt;int\u0026gt;(); if (list.IsEmpty) { ... } Static Extension Members # This is the game changer. You can now add members that appear to be static members of the type itself.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static class EnumerableExtensions { // Extension block with receiver type only (no parameter name) extension\u0026lt;T\u0026gt;(IEnumerable\u0026lt;T\u0026gt;) { public static IEnumerable\u0026lt;T\u0026gt; Identity =\u0026gt; Enumerable.Empty\u0026lt;T\u0026gt;(); // Even operators! public static IEnumerable\u0026lt;T\u0026gt; operator +(IEnumerable\u0026lt;T\u0026gt; left, IEnumerable\u0026lt;T\u0026gt; right) =\u0026gt; left.Concat(right); } } // Usage var combined = listA + listB; // Using the extension operator This feature allows library authors to provide a much more \u0026ldquo;native\u0026rdquo; feel to their helper libraries.\nImplicit Span Conversions # Span\u0026lt;T\u0026gt; and ReadOnlySpan\u0026lt;T\u0026gt; are the backbone of modern high-performance .NET. However, using them often required explicit casting or verbose calls, which added friction to adoption.\nC# 14 introduces first-class support for implicit conversions.\n1 2 3 4 5 6 7 8 9 void ProcessText(ReadOnlySpan\u0026lt;char\u0026gt; text) { ... } string myString = \u0026#34;Hello World\u0026#34;; char[] myChars = [\u0026#39;H\u0026#39;, \u0026#39;i\u0026#39;]; // Before: often needed explicit handling or overloads // C# 14: Just works ProcessText(myString); ProcessText(myChars); This might seem minor, but it removes the API friction that often discouraged developers from using Span in public APIs. It encourages \u0026ldquo;performance by default.\u0026rdquo;\nNull-Conditional Assignment # We\u0026rsquo;ve all written code like this:\n1 2 3 4 if (customer != null) { customer.Order = new Order(); } C# 14 allows us to use the null-conditional operator ?. on the left-hand side of an assignment.\n1 customer?.Order = new Order(); If customer is null, the assignment simply doesn\u0026rsquo;t happen. It also works with compound assignments (like +=), though notably not with increment/decrement operators (++/--).\n1 2 // Only adds 10 if Account is not null user?.Account.Balance += 10; Quick Hits # There are several other improvements worth noting:\nnameof(List\u0026lt;\u0026gt;): You can finally use nameof on unbound generic types. No more nameof(List\u0026lt;int\u0026gt;) just to get the string \u0026ldquo;List\u0026rdquo;. Lambda Modifiers: You can now use modifiers like ref, out, and scoped on lambda parameters without specifying explicit types. 1 var parser = (string s, out int result) =\u0026gt; int.TryParse(s, out result); Partial Constructors \u0026amp; Events: Expanded support for partial members allows source generators to hook into object construction and event handling more effectively. This is a boon for frameworks like MAUI and Blazor that rely heavily on generated code. Final Thoughts # C# 14 is a pragmatic release. It doesn\u0026rsquo;t try to reinvent the paradigm; instead, it looks at how we actually write code and removes the friction. The field keyword alone is going to delete thousands of lines of boilerplate across the ecosystem.\nAs we move to .NET 10, these features allow us to write code that is both more concise and more performant.\nFor more details, check out the official C# 14 announcement and the What\u0026rsquo;s New documentation.\n","date":"4 December 2025","externalUrl":null,"permalink":"/posts/whats-new-in-csharp-14/","section":"Posts","summary":"","title":"What's New in C# 14: A Developer's Guide","type":"posts"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/material-design/","section":"Tags","summary":"","title":"Material Design","type":"tags"},{"content":"WPF is an incredibly powerful framework, but out of the box, it looks\u0026hellip; well, like it\u0026rsquo;s 2010.\nFor years, the community standard for fixing this has been MaterialDesignInXAML (MDIX). It brings Google\u0026rsquo;s Material Design language to WPF with zero effort.\nOn the architectural side, Caliburn.Micro has been a favorite for its \u0026ldquo;Convention over Configuration\u0026rdquo; approach to MVVM.\nBut combining them can sometimes be tricky. How do you handle App.xaml resources? How do MDIX\u0026rsquo;s DialogHost overlays fit with Caliburn\u0026rsquo;s WindowManager?\nIn this post, we\u0026rsquo;ll walk through a complete integration.\n1. The Setup # First, create your WPF project and install the packages.\n1 2 Install-Package Caliburn.Micro Install-Package MaterialDesignThemes 2. The App.xaml Merge # This is the most common point of failure. Caliburn.Micro needs its Bootstrapper initialized in resources, and MDIX needs its dictionaries merged.\nIf you don\u0026rsquo;t merge them correctly, your app will either crash or look like standard Windows 95 controls.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 \u0026lt;Application x:Class=\u0026#34;MyModernApp.App\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:materialDesign=\u0026#34;http://materialdesigninxaml.net/winfx/xaml/themes\u0026#34; xmlns:local=\u0026#34;clr-namespace:MyModernApp\u0026#34;\u0026gt; \u0026lt;Application.Resources\u0026gt; \u0026lt;ResourceDictionary\u0026gt; \u0026lt;ResourceDictionary.MergedDictionaries\u0026gt; \u0026lt;!-- 1. Merge Material Design Dictionaries FIRST --\u0026gt; \u0026lt;!-- Use BundledTheme for cleaner setup (MDIX 4.0+) --\u0026gt; \u0026lt;materialDesign:BundledTheme BaseTheme=\u0026#34;Light\u0026#34; PrimaryColor=\u0026#34;DeepPurple\u0026#34; SecondaryColor=\u0026#34;Lime\u0026#34; /\u0026gt; \u0026lt;!-- Essential Defaults --\u0026gt; \u0026lt;ResourceDictionary Source=\u0026#34;pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml\u0026#34; /\u0026gt; \u0026lt;/ResourceDictionary.MergedDictionaries\u0026gt; \u0026lt;!-- 2. Initialize Caliburn.Micro Bootstrapper --\u0026gt; \u0026lt;local:Bootstrapper x:Key=\u0026#34;Bootstrapper\u0026#34; /\u0026gt; \u0026lt;/ResourceDictionary\u0026gt; \u0026lt;/Application.Resources\u0026gt; \u0026lt;/Application\u0026gt; 3. The ShellView Configuration # In Caliburn.Micro, your main window is usually ShellView.xaml. To get the full Material Design effect (fonts, colors, ripple effects), you need to apply some properties to the Window itself.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;Window x:Class=\u0026#34;MyModernApp.Views.ShellView\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:materialDesign=\u0026#34;http://materialdesigninxaml.net/winfx/xaml/themes\u0026#34; \u0026lt;!-- Apply the Material Design Font and Colors globally --\u0026gt; TextElement.Foreground=\u0026#34;{DynamicResource MaterialDesignBody}\u0026#34; TextElement.FontWeight=\u0026#34;Regular\u0026#34; TextElement.FontSize=\u0026#34;13\u0026#34; TextOptions.TextFormattingMode=\u0026#34;Ideal\u0026#34; FontFamily=\u0026#34;{DynamicResource MaterialDesignFont}\u0026#34; \u0026lt;!-- Optional: Use the MD Window Style for a cleaner chrome --\u0026gt; Style=\u0026#34;{StaticResource MaterialDesignWindow}\u0026#34; Title=\u0026#34;My Modern App\u0026#34; Height=\u0026#34;450\u0026#34; Width=\u0026#34;800\u0026#34;\u0026gt; \u0026lt;!-- Your Content Here --\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;ContentControl x:Name=\u0026#34;ActiveItem\u0026#34; /\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Window\u0026gt; 4. The Dialog Dilemma: WindowManager vs. DialogHost # This is the biggest architectural clash.\nCaliburn.Micro uses IWindowManager.ShowDialogAsync() to open a new Window (a modal). Material Design encourages DialogHost, which is an overlay on the existing window. Overlays generally look more modern and \u0026ldquo;web-like\u0026rdquo; than popping up separate OS windows.\nThe Solution: A Dialog Service # Don\u0026rsquo;t call DialogHost.Show directly from your ViewModel (that breaks MVVM). Instead, create a service.\nThe Interface:\n1 2 3 4 5 public interface IDialogService { Task ShowMessage(string message); Task\u0026lt;bool\u0026gt; ShowConfirmation(string message); } The Implementation:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using MaterialDesignThemes.Wpf; public class MaterialDialogService : IDialogService { public async Task ShowMessage(string message) { // Create a simple view for the message var view = new TextBlock { Text = message, Margin = new Thickness(20) }; // \u0026#34;RootDialog\u0026#34; is the Identifier we will set in XAML await DialogHost.Show(view, \u0026#34;RootDialog\u0026#34;); } // ... implement confirmation similarly } The ShellView Update: Wrap your content in a DialogHost.\n1 2 3 4 5 \u0026lt;materialDesign:DialogHost Identifier=\u0026#34;RootDialog\u0026#34;\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;ContentControl x:Name=\u0026#34;ActiveItem\u0026#34; /\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/materialDesign:DialogHost\u0026gt; Now your ViewModels can inject IDialogService and show beautiful overlays without knowing about XAML.\n5. Validation that Pops # One of the best features of MDIX is the input validation styles. Caliburn.Micro supports IDataErrorInfo out of the box.\nViewModel:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class LoginViewModel : Screen, IDataErrorInfo { private string _username; public string Username { get =\u0026gt; _username; set { _username = value; NotifyOfPropertyChange(() =\u0026gt; Username); } } public string this[string columnName] { get { if (columnName == nameof(Username) \u0026amp;\u0026amp; string.IsNullOrWhiteSpace(Username)) return \u0026#34;Username is required!\u0026#34;; return null; } } public string Error =\u0026gt; null; } View: Just bind it. MDIX handles the rest automatically, showing the error text in red below the line.\n1 2 3 \u0026lt;TextBox Text=\u0026#34;{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}\u0026#34; materialDesign:HintAssist.Hint=\u0026#34;Username\u0026#34; Style=\u0026#34;{StaticResource MaterialDesignFloatingHintTextBox}\u0026#34; /\u0026gt; 6. Dynamic Theming # Want to let the user switch between Light and Dark mode? MDIX makes this trivial with the PaletteHelper.\n1 2 3 4 5 6 7 8 public void ToggleTheme() { var paletteHelper = new PaletteHelper(); var theme = paletteHelper.GetTheme(); theme.SetBaseTheme(theme.GetBaseTheme() == BaseTheme.Dark ? BaseTheme.Light : BaseTheme.Dark); paletteHelper.SetTheme(theme); } Summary # Integrating MaterialDesignInXAML with Caliburn.Micro gives you the best of both worlds: a robust, testable architecture and a stunning, modern UI.\nThe key takeaways are:\nMerge Resources Correctly: MDIX first, then Bootstrapper. Use DialogHost: Prefer overlays over separate windows for a modern feel. Leverage Styles: Use HintAssist and ValidatesOnDataErrors to make your forms sing. Your WPF application doesn\u0026rsquo;t have to look like it was built in 2010. With these tools, you can create beautiful, modern interfaces that your users will love—and that your development team can maintain and test with confidence.\nFurther Reading # MaterialDesignInXAML on GitHub - Official repository Caliburn.Micro Documentation - Official documentation Material Design Guidelines - Google\u0026rsquo;s Material Design principles WPF MVVM Patterns - Microsoft\u0026rsquo;s MVVM guide ","date":"3 December 2025","externalUrl":null,"permalink":"/posts/modernizing-wpf-material-design-caliburn-micro/","section":"Posts","summary":"","title":"Modernizing WPF: Integrating MaterialDesignInXAML with Caliburn.Micro","type":"posts"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/categories/ui/","section":"Categories","summary":"","title":"UI","type":"categories"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/ui/ux/","section":"Tags","summary":"","title":"UI/UX","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/xaml/","section":"Tags","summary":"","title":"XAML","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/mef/","section":"Tags","summary":"","title":"MEF","type":"tags"},{"content":"Building extensible applications is a rite of passage for many .NET developers. Whether you\u0026rsquo;re building a dashboard that needs dynamic widgets or a creative tool that supports third-party effects, you eventually hit the \u0026ldquo;Plugin Problem.\u0026rdquo;\nIn the WPF ecosystem, two architectural patterns have dominated this space: MEF (Managed Extensibility Framework) and Caliburn.Micro\u0026rsquo;s Parent/Child Bootstrappers.\nBut regardless of which one you choose, you will eventually face the final boss of modularity: Dependency Conflicts.\nIn this post, we\u0026rsquo;ll compare these two approaches and explore how modern .NET features like AssemblyLoadContext allow us to achieve true plugin isolation.\nThe Contenders # 1. MEF (Managed Extensibility Framework) # MEF has been the standard for .NET extensibility since .NET Framework 4.0. It relies on a declarative model using attributes.\nPhilosophy: \u0026ldquo;I have a hole here ([Import]), please fill it with a matching peg ([Export]).\u0026rdquo; Pros: Deeply integrated into the ecosystem, supports metadata (lazy loading plugins without instantiating them), and handles complex dependency graphs automatically. Cons: Can feel \u0026ldquo;magical\u0026rdquo; and hard to debug when composition fails. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using System.ComponentModel.Composition; // The Contract public interface IPlugin { void Execute(); } // The Plugin [Export(typeof(IPlugin))] public class MyCoolPlugin : IPlugin { public void Execute() =\u0026gt; Console.WriteLine(\u0026#34;Plugin running...\u0026#34;); } // The Host public class PluginHost { [ImportMany] public IEnumerable\u0026lt;IPlugin\u0026gt; Plugins { get; set; } } 2. Caliburn.Micro (Parent/Child Bootstrappers) # Caliburn.Micro is a powerful MVVM framework. Its approach to modularity is less about \u0026ldquo;plugins\u0026rdquo; and more about \u0026ldquo;composition of screens.\u0026rdquo;\nPhilosophy: The application is a tree of ViewModels. A \u0026ldquo;Module\u0026rdquo; is just a child container that registers its own ViewModels and Services. Pros: Extremely natural if you are already doing MVVM. It handles the UI composition (View location) automatically. Cons: Tends to be more coupled to the specific MVVM framework. In this architecture, you typically override SelectAssemblies in your Bootstrapper to tell Caliburn where to look for ViewModels.\nThe Problem: Dependency Hell # Imagine this scenario:\nHost App uses Newtonsoft.Json v13.0. Plugin A uses Newtonsoft.Json v13.0. (All good). Plugin B is older and references Newtonsoft.Json v9.0. In a standard .NET application, you can only load one version of an assembly into the default context. When Plugin B tries to run, it might crash with a MethodNotFoundException because it got v13 instead of v9, or the load itself might fail.\nThe Solution: AssemblyLoadContext (ALC) # Since .NET Core (.NET 5+), we have System.Runtime.Loader.AssemblyLoadContext. This allows us to load assemblies into isolated \u0026ldquo;islands.\u0026rdquo;\nBy loading each plugin into its own ALC, Plugin A can have its version of JSON.NET, and Plugin B can have its own version, side-by-side in the same process.\nImplementing Isolation # First, we need a custom context that knows how to resolve dependencies relative to the plugin\u0026rsquo;s location.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using System.Reflection; using System.Runtime.Loader; public class PluginLoadContext : AssemblyLoadContext { private AssemblyDependencyResolver _resolver; public PluginLoadContext(string pluginPath) : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly Load(AssemblyName assemblyName) { string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; } } Strategy 1: MEF with ALC # To use this with MEF, we can\u0026rsquo;t just point a DirectoryCatalog at a folder. We need to manually load the assemblies into our ALC, and then pass those assemblies to MEF.\nNote: This example uses System.ComponentModel.Composition (MEF 1). If you are using System.Composition (MEF 2 / Lightweight MEF), the container setup is slightly different (ContainerConfiguration), but the ALC principle remains the same.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System.ComponentModel.Composition.Hosting; var catalogs = new AggregateCatalog(); foreach (var pluginPath in Directory.GetFiles(\u0026#34;Plugins\u0026#34;, \u0026#34;*.dll\u0026#34;)) { var alc = new PluginLoadContext(pluginPath); var assembly = alc.LoadFromAssemblyPath(pluginPath); // Add the assembly to MEF\u0026#39;s catalog var assemblyCatalog = new AssemblyCatalog(assembly); catalogs.Catalogs.Add(assemblyCatalog); } var container = new CompositionContainer(catalogs); // Now MEF composes using types from isolated contexts! Strategy 2: Caliburn.Micro with ALC # For Caliburn.Micro, we hook into the SelectAssemblies method in our Bootstrapper.\n1 2 3 4 5 6 7 8 9 10 11 12 13 protected override IEnumerable\u0026lt;Assembly\u0026gt; SelectAssemblies() { var assemblies = base.SelectAssemblies().ToList(); foreach (var pluginPath in Directory.GetFiles(\u0026#34;Plugins\u0026#34;, \u0026#34;*.dll\u0026#34;)) { var alc = new PluginLoadContext(pluginPath); var assembly = alc.LoadFromAssemblyPath(pluginPath); assemblies.Add(assembly); } return assemblies; } Crucial Note for Caliburn: Since Caliburn relies heavily on reflection and type equality, you must ensure that the Contract Interfaces (e.g., IShell, IModule) are shared. They must be loaded in the Default Context (the host), not loaded again inside the Plugin Context.\nThe AssemblyDependencyResolver usually handles this correctly: if the host already has the assembly, it won\u0026rsquo;t resolve it from the plugin folder, allowing the runtime to fall back to the default context.\nConclusion # Use MEF if your application is data-centric or service-centric, and you need complex composition rules. Use Caliburn.Micro if your plugins are primarily UI screens (ViewModels/Views) and you want a seamless MVVM experience. In either case, if you are distributing plugins that might have conflicting dependencies, wrapping them in an AssemblyLoadContext is the modern, robust way to keep your application stable.\nFurther Reading # MEF (Managed Extensibility Framework) - Official Microsoft documentation Caliburn.Micro Documentation - Official Caliburn.Micro docs AssemblyLoadContext - Understanding plugin isolation Plugin Architecture Patterns - Creating apps with plugin support ","date":"3 December 2025","externalUrl":null,"permalink":"/posts/mef-vs-caliburn-micro-plugin-architecture/","section":"Posts","summary":"","title":"Plugin Architectures in .NET: MEF vs. Caliburn.Micro \u0026 Dependency Isolation","type":"posts"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/plugins/","section":"Tags","summary":"","title":"Plugins","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/devops/","section":"Tags","summary":"","title":"DevOps","type":"tags"},{"content":"Large Language Models (LLMs) are brilliant reasoning engines, but they live in a box. They can\u0026rsquo;t restart a server, check disk space, or query your internal APIs—unless you give them Plugins.\nIn this post, we\u0026rsquo;ll move beyond the basic \u0026ldquo;Hello World\u0026rdquo; examples and build a practical System Administration Plugin using Semantic Kernel.\nThe \u0026ldquo;Brain in a Jar\u0026rdquo; Problem # Imagine hiring a brilliant sysadmin who is locked in a room with no computer. They can tell you how to fix a server, but they can\u0026rsquo;t actually do it.\nPlugins bridge this gap. They act as the \u0026ldquo;hands\u0026rdquo; of the AI, allowing it to:\nRead State: Fetch real-time data (e.g., \u0026ldquo;Is the database healthy?\u0026rdquo;). Take Action: Execute commands (e.g., \u0026ldquo;Restart the IIS service\u0026rdquo;). Offload Logic: Perform deterministic calculations that LLMs often get wrong. Building a \u0026ldquo;SysAdmin\u0026rdquo; Plugin # Let\u0026rsquo;s build a plugin that allows an AI agent to monitor and manage servers. We\u0026rsquo;ll define two functions: one to check disk space and another to restart services.\n1. Define the Plugin Class # The core of a plugin is a standard C# class. We use the [KernelFunction] attribute to expose methods to the AI, and [Description] to tell the AI when and how to use them.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 using Microsoft.SemanticKernel; using System.ComponentModel; public class ServerManagementPlugin { [KernelFunction] [Description(\u0026#34;Checks the available disk space on a specific server.\u0026#34;)] public string GetDiskSpace( [Description(\u0026#34;The name of the server (e.g., WEB-01, DB-02)\u0026#34;)] string serverName) { // In a real app, this would query WMI or an infrastructure API. // We\u0026#39;ll simulate a response for this demo. return serverName.ToUpper() switch { \u0026#34;WEB-01\u0026#34; =\u0026gt; \u0026#34;45% free (200GB)\u0026#34;, \u0026#34;DB-01\u0026#34; =\u0026gt; \u0026#34;5% free (CRITICAL)\u0026#34;, _ =\u0026gt; \u0026#34;Server not found\u0026#34; }; } [KernelFunction] [Description(\u0026#34;Restarts a specific service on a target server.\u0026#34;)] public string RestartService( [Description(\u0026#34;The name of the service to restart\u0026#34;)] string serviceName, [Description(\u0026#34;The server to perform the action on\u0026#34;)] string serverName) { Console.WriteLine($\u0026#34;[AUDIT] Restarting {serviceName} on {serverName}...\u0026#34;); // Simulate the restart delay return $\u0026#34;Service \u0026#39;{serviceName}\u0026#39; on \u0026#39;{serverName}\u0026#39; has been successfully restarted.\u0026#34;; } } 2. Register and Run the Kernel # Now we need to register this plugin with the Kernel and let the AI use it. Notice how we don\u0026rsquo;t explicitly call the functions; the AI decides to call them based on our prompt.\nPrerequisites:\n1 dotnet add package Microsoft.SemanticKernel The Code:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; // 1. Initialize the Builder var builder = Kernel.CreateBuilder(); // 2. Add your LLM Service (Azure OpenAI or OpenAI) // Always use environment variables or secrets management in production! builder.AddAzureOpenAIChatCompletion( deploymentName: \u0026#34;gpt-4o\u0026#34;, endpoint: Environment.GetEnvironmentVariable(\u0026#34;AZURE_OPENAI_ENDPOINT\u0026#34;), apiKey: Environment.GetEnvironmentVariable(\u0026#34;AZURE_OPENAI_KEY\u0026#34;)); // 3. Register the Plugin builder.Plugins.AddFromType\u0026lt;ServerManagementPlugin\u0026gt;(\u0026#34;SysAdmin\u0026#34;); var kernel = builder.Build(); // 4. Enable Automatic Function Calling // This setting tells the AI it\u0026#39;s allowed to \u0026#34;use its tools\u0026#34; automatically. OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // 5. The Scenario // We give the AI a high-level goal. It should figure out it needs to: // 1. Check disk space on DB-01. // 2. See that it\u0026#39;s low. // 3. (Hypothetically) decide to restart a cleanup service or just report back. // Let\u0026#39;s try a direct command first. var prompt = \u0026#34;I\u0026#39;m getting alerts for DB-01. Check its disk status, and if it\u0026#39;s critical, restart the \u0026#39;LogArchiver\u0026#39; service.\u0026#34;; Console.WriteLine($\u0026#34;User: {prompt}\u0026#34;); var result = await kernel.InvokePromptAsync(prompt, new(settings)); Console.WriteLine($\u0026#34;Assistant: {result}\u0026#34;); Expected Output # When you run this, the interaction happens in a loop (handled by AutoInvokeKernelFunctions):\nAI Thought: \u0026ldquo;I need to check disk space for DB-01.\u0026rdquo; -\u0026gt; Calls GetDiskSpace(\u0026quot;DB-01\u0026quot;). Plugin Output: \u0026ldquo;5% free (CRITICAL)\u0026rdquo;. AI Thought: \u0026ldquo;The user said if it\u0026rsquo;s critical, restart \u0026lsquo;LogArchiver\u0026rsquo;.\u0026rdquo; -\u0026gt; Calls RestartService(\u0026quot;LogArchiver\u0026quot;, \u0026quot;DB-01\u0026quot;). Plugin Output: \u0026ldquo;Service \u0026lsquo;LogArchiver\u0026rsquo; on \u0026lsquo;DB-01\u0026rsquo; has been successfully restarted.\u0026rdquo; AI Final Response: \u0026ldquo;I checked DB-01 and found disk space was critical (5% free). As requested, I have restarted the \u0026lsquo;LogArchiver\u0026rsquo; service.\u0026rdquo; Best Practices for Plugins # Descriptive Names: The [Description] attribute is your API documentation for the AI. Be verbose. If a parameter format matters (e.g., \u0026ldquo;YYYY-MM-DD\u0026rdquo;), say so in the description. Keep it Deterministic: Plugins should ideally be reliable tools. If a plugin fails, ensure it returns a clear error message string so the AI can understand what went wrong and tell the user. Security First: Never expose dangerous functions (like DeleteDatabase) without a \u0026ldquo;human in the loop\u0026rdquo; confirmation step. You can implement this by having the plugin return a \u0026ldquo;Confirmation required\u0026rdquo; message instead of executing immediately. Pro Tip: Testing Your Plugins # Because plugins are just C# classes, you can (and should) write standard unit tests for them! You don\u0026rsquo;t need the AI to test the logic inside GetDiskSpace. Test the deterministic code deterministically, and trust the Kernel to handle the routing.\nBy structuring your code as plugins, you transform your application from a text generator into an intelligent automation platform.\nFurther Reading # Semantic Kernel on GitHub - The official repository with examples Creating Semantic Kernel Plugins - Official documentation Function Calling in OpenAI - Understanding the underlying mechanism ","date":"3 December 2025","externalUrl":null,"permalink":"/posts/semantic-kernel-plugins/","section":"Posts","summary":"","title":"Mastering Semantic Kernel Plugins","type":"posts"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/semantic-kernel/","section":"Tags","summary":"","title":"Semantic Kernel","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/agent-framework/","section":"Tags","summary":"","title":"Agent Framework","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/agents/","section":"Tags","summary":"","title":"Agents","type":"tags"},{"content":"The world of AI development is evolving fast. Microsoft has recently introduced the Microsoft Agent Framework, a new open-source development kit that serves as the unified successor to both Semantic Kernel and AutoGen.\nIf you\u0026rsquo;ve been exploring agentic AI, you might be asking: Why another framework?\nThe Unification of AI Development # Built by the same teams behind Semantic Kernel and AutoGen, the Microsoft Agent Framework combines the best of both worlds:\nFrom AutoGen: Simple abstractions for powerful multi-agent orchestration and conversation patterns. From Semantic Kernel: Enterprise-grade features like thread-based state management, type safety, telemetry, and robust model support. It is designed to be the foundation for building AI agents going forward, available for both .NET and Python.\nCore Concepts # The framework is built around two primary capabilities:\n1. AI Agents # An AI Agent is an autonomous unit that uses Large Language Models (LLMs) to process input, make decisions, and take action.\nTools \u0026amp; MCP: Agents can connect to external tools and Model Context Protocol (MCP) servers to interact with the real world. Flexibility: They support various providers including Azure OpenAI, OpenAI, and Azure AI. 2. Workflows # Workflows allow you to chain agents and functions together to solve complex problems.\nGraph-Based: Define explicit execution paths, loops, and conditional routing. Type Safety: Ensure data flows correctly between agents with compile-time checks. Checkpointing: Save the state of a workflow, enabling long-running processes that can pause and resume (perfect for human-in-the-loop scenarios). Getting Started # To start building with the Microsoft Agent Framework in .NET, you\u0026rsquo;ll need the .NET 8.0 SDK and an Azure OpenAI resource.\n1. Create a Project # First, create a new console application:\n1 2 dotnet new console -o AgentFrameworkQuickStart cd AgentFrameworkQuickStart 2. Install Packages # Add the necessary NuGet packages. Note that these are currently in prerelease:\n1 2 3 dotnet add package Azure.AI.OpenAI --prerelease dotnet add package Azure.Identity dotnet add package Microsoft.Agents.AI.OpenAI --prerelease 3. Create a Basic Agent # Here is a simple example of creating an agent that uses Azure OpenAI to tell jokes. This code uses AzureCliCredential for authentication, so make sure you are logged in via az login.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using System; using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using OpenAI; // Initialize the agent with Azure OpenAI AIAgent agent = new AzureOpenAIClient( new Uri(\u0026#34;https://your-resource.openai.azure.com/\u0026#34;), new AzureCliCredential()) .GetChatClient(\u0026#34;gpt-4o-mini\u0026#34;) .CreateAIAgent(instructions: \u0026#34;You are good at telling jokes.\u0026#34;); // Run the agent Console.WriteLine(await agent.RunAsync(\u0026#34;Tell me a joke about a pirate.\u0026#34;)); 4. Chaining Agents: A Sequential Workflow # One of the most powerful features is the ability to chain agents together. In this example, we\u0026rsquo;ll create a sequential pipeline where a message is passed through multiple translation agents.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using Microsoft.Agents.Workflows; using Microsoft.Extensions.AI; // 1. Setup the client var chatClient = new AzureOpenAIClient( new Uri(\u0026#34;https://your-resource.openai.azure.com/\u0026#34;), new AzureCliCredential()) .GetChatClient(\u0026#34;gpt-4o-mini\u0026#34;); // 2. Define specialized agents // Helper to create a translator agent static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient client) =\u0026gt; new(client, $\u0026#34;You are a translation assistant who only responds in {targetLanguage}. \u0026#34; + $\u0026#34;Translate the input to {targetLanguage}.\u0026#34;); // Create a chain: English -\u0026gt; French -\u0026gt; Spanish -\u0026gt; English var translationAgents = new[] { GetTranslationAgent(\u0026#34;French\u0026#34;, chatClient), GetTranslationAgent(\u0026#34;Spanish\u0026#34;, chatClient), GetTranslationAgent(\u0026#34;English\u0026#34;, chatClient) }; // 3. Build the sequential workflow var workflow = AgentWorkflowBuilder.BuildSequential(translationAgents); // 4. Run the workflow var messages = new List\u0026lt;ChatMessage\u0026gt; { new(ChatRole.User, \u0026#34;Hello, world!\u0026#34;) }; StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages); // IMPORTANT: Signal the workflow to start processing await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); // 5. Process the output await foreach (WorkflowEvent evt in run.WatchStreamAsync()) { if (evt is AgentRunUpdateEvent e) { Console.WriteLine($\u0026#34;{e.ExecutorId}: {e.Data}\u0026#34;); } } What happens here?\nThe user says \u0026ldquo;Hello, world!\u0026rdquo;. The French Agent translates it to \u0026ldquo;Bonjour, le monde !\u0026rdquo;. The Spanish Agent takes that French output and translates it to \u0026ldquo;¡Hola, mundo!\u0026rdquo;. The English Agent translates it back to \u0026ldquo;Hello, world!\u0026rdquo;. This pattern is perfect for multi-stage processing, such as Draft -\u0026gt; Review -\u0026gt; Polish pipelines.\n5. Advanced Orchestration: Dynamic Handoffs # While sequential chains are useful, real-world tasks often require dynamic decision-making. Handoff Orchestration allows agents to transfer control to one another based on the conversation context.\nImagine a customer support bot that needs to route queries to specialists:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 1. Setup the client var client = new AzureOpenAIClient( new Uri(\u0026#34;https://your-resource.openai.azure.com/\u0026#34;), new AzureCliCredential()) .GetChatClient(\u0026#34;gpt-4o-mini\u0026#34;); // 2. Define specialized agents ChatClientAgent mathAgent = new(client, \u0026#34;You are a math tutor. Solve the problem and explain your steps.\u0026#34;, \u0026#34;MathAgent\u0026#34;); ChatClientAgent historyAgent = new(client, \u0026#34;You are a historian. Provide context and dates for events.\u0026#34;, \u0026#34;HistoryAgent\u0026#34;); ChatClientAgent triageAgent = new(client, \u0026#34;Analyze the user\u0026#39;s request. If it is about math, handoff to MathAgent. \u0026#34; + \u0026#34;If it is about history, handoff to HistoryAgent. Otherwise, answer directly.\u0026#34;, \u0026#34;TriageAgent\u0026#34;); // 3. Build the dynamic workflow var workflow = AgentWorkflowBuilder.StartHandoffWith(triageAgent) .WithHandoffs(triageAgent, [mathAgent, historyAgent]) // Triage can route to specialists .WithHandoff(mathAgent, triageAgent) // Specialists return control to Triage .WithHandoff(historyAgent, triageAgent) .Build(); In this workflow, the TriageAgent acts as a router. If the user asks \u0026ldquo;What is the derivative of x^2?\u0026rdquo;, the TriageAgent recognizes the intent and hands off execution to the MathAgent. This mimics how human teams delegate tasks based on expertise.\nA Simple Mental Model # When building an app, you will typically:\nDefine Agents: Create agents with specific personas and tools (e.g., a \u0026ldquo;Researcher\u0026rdquo; agent with web search capabilities). Create a Workflow: Define how these agents interact. Does the Researcher pass info to a Writer? Does a Manager review the output? Run \u0026amp; Monitor: Execute the workflow, utilizing built-in telemetry to debug and optimize performance. Why Switch? # If you are building enterprise applications, the Agent Framework offers critical advantages:\nReliability: With strong typing and state management, it\u0026rsquo;s easier to build robust systems that don\u0026rsquo;t \u0026ldquo;hallucinate\u0026rdquo; process steps. Scalability: The workflow engine is designed to handle complex, multi-step tasks that would overwhelm a single agent. Future-Proof: As the direct successor, this framework will receive the latest features and improvements from Microsoft\u0026rsquo;s AI teams. Further Reading # Microsoft Agent Framework Documentation - Official documentation Agent Framework on GitHub - Source code and examples (part of Semantic Kernel repo) Multi-Agent Systems - Concepts and patterns Migration from Semantic Kernel - Migration guide ","date":"3 December 2025","externalUrl":null,"permalink":"/posts/microsoft-agent-framework-intro/","section":"Posts","summary":"","title":"Getting Started with Microsoft Agent Framework","type":"posts"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/microsoft/","section":"Tags","summary":"","title":"Microsoft","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/categories/tech-trends/","section":"Categories","summary":"","title":"Tech Trends","type":"categories"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/automation/","section":"Tags","summary":"","title":"Automation","type":"tags"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/flaui/","section":"Tags","summary":"","title":"FlaUI","type":"tags"},{"content":"If you\u0026rsquo;ve been working in the .NET ecosystem for a while, you know that WPF (Windows Presentation Foundation) is far from dead. It powers a massive amount of enterprise desktop software. But let\u0026rsquo;s be honest: testing desktop UI has historically been\u0026hellip; painful.\nMicrosoft\u0026rsquo;s CodedUI is long gone, and WinAppDriver has had a rocky road. Enter FlaUI.\nIn this post, we\u0026rsquo;re going to look at how to set up a robust testing framework for WPF using FlaUI, why AutomationProperties are your best friend, and how we can leverage AI tools like GitHub Copilot to make writing these tests significantly faster.\nWhy FlaUI? # FlaUI is a .NET library that helps with automated UI testing of Windows applications (WPF, WinForms, etc.). It wraps Microsoft\u0026rsquo;s UI Automation libraries (UIA2 and UIA3) in a clean, fluent API.\nUnlike some older tools that relied on coordinate clicks or fragile image recognition, FlaUI interacts with the visual tree of your application, making your tests much more resilient to resolution changes and theming.\nThe Golden Rule: AutomationProperties # Before we write a single line of C#, we need to talk about XAML.\nThe most common cause of \u0026ldquo;flaky\u0026rdquo; UI tests is relying on text labels or dynamic content to find elements. If you find a button by its text \u0026ldquo;Save\u0026rdquo;, and someone changes it to \u0026ldquo;Submit\u0026rdquo;, your test fails.\nThe solution is AutomationProperties.AutomationId.\nBy assigning a stable, unique ID to your interactive elements, you decouple your tests from the visual presentation.\n1 2 3 4 5 6 7 \u0026lt;!-- Don\u0026#39;t do this if you can help it --\u0026gt; \u0026lt;Button Content=\u0026#34;Save Changes\u0026#34; Command=\u0026#34;{Binding SaveCommand}\u0026#34; /\u0026gt; \u0026lt;!-- Do this! --\u0026gt; \u0026lt;Button AutomationProperties.AutomationId=\u0026#34;BtnSaveChanges\u0026#34; Content=\u0026#34;Save Changes\u0026#34; Command=\u0026#34;{Binding SaveCommand}\u0026#34; /\u0026gt; This ID is invisible to the user but clearly visible to FlaUI.\nSetting Up Your Test Project # Let\u0026rsquo;s get practical. You\u0026rsquo;ll want a separate test project (e.g., NUnit or xUnit).\nCreate a new NUnit Test Project. Install the following NuGet packages: FlaUI.Core FlaUI.UIA3 (UIA3 is generally recommended for WPF) Here is a basic skeleton of a test that launches an app and clicks a button:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using FlaUI.Core; using FlaUI.UIA3; using NUnit.Framework; [TestFixture] public class LoginTests { [Test] public void CanClickLoginButton() { // 1. Launch the application var appPath = @\u0026#34;C:\\Path\\To\\YourApp.exe\u0026#34;; using var app = Application.Launch(appPath); // 2. Initialize the automation engine using var automation = new UIA3Automation(); // 3. Get the main window var window = app.GetMainWindow(automation); // 4. Find element by ID (The robust way!) // Tip: In real scenarios, UI takes time to load. Use Retry.WhileNull() to wait for elements. var btn = window.FindFirstDescendant(cf =\u0026gt; cf.ByAutomationId(\u0026#34;BtnLogin\u0026#34;))?.AsButton(); Assert.IsNotNull(btn, \u0026#34;Could not find login button\u0026#34;); // 5. Interact btn.Invoke(); } } Supercharging with AI # Writing UI tests can be tedious. You have to look up IDs, map them to objects, and write boilerplate code. This is where AI tools like GitHub Copilot shine.\n1. Generating Automation IDs # If you have a complex XAML view without IDs, you can paste the XAML into your chat with Copilot and ask:\n\u0026ldquo;Add AutomationProperties.AutomationId to all interactive controls in this XAML using a consistent naming convention.\u0026rdquo;\nIt will return your XAML with BtnSubmit, TxtUsername, LstItems etc., saving you the manual typing.\n2. Scaffolding Page Objects # The Page Object Pattern is a best practice where you create a class that represents a specific screen (e.g., LoginPage). This class handles finding elements, keeping your tests clean.\nYou can provide your XAML to the AI and ask:\n\u0026ldquo;Create a FlaUI Page Object class for this XAML view. Expose the interactive elements as properties.\u0026rdquo;\nThe AI can generate something like this in seconds:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using FlaUI.Core; using FlaUI.Core.AutomationElements; public class LoginPage : Window { public LoginPage(FrameworkAutomationElementBase element) : base(element) { } public TextBox UsernameBox =\u0026gt; FindFirstDescendant(cf =\u0026gt; cf.ByAutomationId(\u0026#34;TxtUsername\u0026#34;))?.AsTextBox(); public TextBox PasswordBox =\u0026gt; FindFirstDescendant(cf =\u0026gt; cf.ByAutomationId(\u0026#34;TxtPassword\u0026#34;))?.AsTextBox(); public Button LoginButton =\u0026gt; FindFirstDescendant(cf =\u0026gt; cf.ByAutomationId(\u0026#34;BtnLogin\u0026#34;))?.AsButton(); public void Login(string user, string pass) { UsernameBox.Text = user; PasswordBox.Text = pass; LoginButton.Invoke(); } } 3. Writing the Test Logic # Once you have your Page Objects, you can ask the AI to write the actual test scenarios:\n\u0026ldquo;Write an NUnit test using the LoginPage object that verifies an error message appears when the password is empty.\u0026rdquo;\nSummary # Testing WPF applications doesn\u0026rsquo;t have to be a nightmare of fragile scripts. By combining FlaUI for robust automation, AutomationProperties for stability, and AI to handle the boilerplate, you can build a high-quality test suite that actually saves you time. Your future self (and your team) will thank you when that critical refactor doesn\u0026rsquo;t break the entire application.\nFurther Reading # FlaUI on GitHub - Official FlaUI repository UI Automation Overview - Microsoft\u0026rsquo;s UI Automation documentation AutomationProperties in WPF - API reference Page Object Pattern - Martin Fowler\u0026rsquo;s explanation ","date":"3 December 2025","externalUrl":null,"permalink":"/posts/flaui-testing-wpf-ai/","section":"Posts","summary":"","title":"Modern WPF Testing with FlaUI and AI","type":"posts"},{"content":"","date":"3 December 2025","externalUrl":null,"permalink":"/tags/testing/","section":"Tags","summary":"","title":"Testing","type":"tags"},{"content":"","date":"29 November 2025","externalUrl":null,"permalink":"/tags/ci/cd/","section":"Tags","summary":"","title":"CI/CD","type":"tags"},{"content":"","date":"29 November 2025","externalUrl":null,"permalink":"/categories/devops/","section":"Categories","summary":"","title":"DevOps","type":"categories"},{"content":"","date":"29 November 2025","externalUrl":null,"permalink":"/tags/github-actions/","section":"Tags","summary":"","title":"GitHub Actions","type":"tags"},{"content":"","date":"29 November 2025","externalUrl":null,"permalink":"/categories/testing/","section":"Categories","summary":"","title":"Testing","type":"categories"},{"content":"You\u0026rsquo;ve written your FlaUI tests. They pass locally. You push to GitHub. The build fails.\nWelcome to the world of UI automation in CI/CD.\nRunning desktop UI tests in a cloud environment like GitHub Actions is notoriously difficult. Unlike unit tests, which just need a CPU and memory, UI tests need a Desktop Session.\nIn this post, we\u0026rsquo;ll tackle the challenges of running FlaUI tests on GitHub Actions windows-latest runners and how to make them reliable.\nThe Environment: What is windows-latest? # When you spin up a GitHub Action with runs-on: windows-latest, you are getting a Virtual Machine. Crucially, this VM does have a desktop session, but it is \u0026ldquo;headless\u0026rdquo; (no physical monitor connected).\nThe default resolution is typically 1024x768.\nIf your application requires a 1080p screen to render all buttons, your tests will fail because elements might be scrolled out of view or collapsed into a \u0026ldquo;hamburger\u0026rdquo; menu that your test doesn\u0026rsquo;t expect.\nStep 1: The Workflow YAML # Here is a baseline workflow for running .NET tests.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 name: Desktop UI Tests on: [push] jobs: test: runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Build run: dotnet build MyWpfApp.sln --configuration Release - name: Run FlaUI Tests run: dotnet test MyWpfApp.Tests/MyWpfApp.Tests.csproj --configuration Release --no-build --logger \u0026#34;trx;LogFileName=test_results.trx\u0026#34; This might work if your app is simple. But when it fails, you\u0026rsquo;ll have no idea why.\nStep 2: The \u0026ldquo;Black Box\u0026rdquo; Problem (Screenshots) # When a test fails locally, you watch it happen. In CI, you just get a stack trace saying ElementNotFoundException.\nTo fix this, you must configure your tests to take a screenshot on failure. FlaUI makes this easy, but you need to hook it into your test framework (e.g., NUnit TearDown).\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using NUnit.Framework; using NUnit.Framework.Interfaces; using System.IO; using FlaUI.Core.Capturing; [TearDown] public void TearDown() { if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed) { var screenshotPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, $\u0026#34;Fail_{TestContext.CurrentContext.Test.Name}.png\u0026#34;); // Capture the full desktop var image = Capture.Screen(); image.ToFile(screenshotPath); TestContext.AddTestAttachment(screenshotPath); } // Ensure the app is closed/killed so it doesn\u0026#39;t block the next test _app?.Dispose(); _automation?.Dispose(); } Step 3: Uploading Artifacts # Now that you\u0026rsquo;re saving screenshots, you need to tell GitHub Actions to save them so you can download them after the run.\nUpdate your YAML to include an upload-artifact step that runs even if tests fail.\n1 2 3 4 5 6 7 8 9 - name: Upload Test Screenshots if: failure() uses: actions/upload-artifact@v4 with: name: test-failures path: | **/*.png **/*.trx if-no-files-found: ignore Step 4: Handling Screen Resolution # If 1024x768 is breaking your app, you have two options:\nDesign for Responsiveness: Make your app (and tests) handle small screens. This is the \u0026ldquo;correct\u0026rdquo; software engineering answer. Change the Resolution: You can try to use tools to change the VM resolution, but on GitHub hosted runners, this is often locked or unreliable. A better hack is to ensure your Application.Launch code maximizes the window, or sets a specific size that fits within 1024x768.\n1 2 3 4 5 6 7 8 9 10 11 using FlaUI.Core; using FlaUI.UIA3; // In your Setup var app = Application.Launch(appPath); var automation = new UIA3Automation(); var window = app.GetMainWindow(automation); // Force a known size that fits in the default runner resolution window.Move(0, 0); window.Resize(1024, 768); Step 5: The \u0026ldquo;Active Session\u0026rdquo; Myth # You might read online that you need a \u0026ldquo;Self-Hosted Runner\u0026rdquo; with an active RDP session to run UI tests.\nFor FlaUI (UIA3), this is not strictly true. UIA3 works fine in the non-interactive session provided by GitHub Actions for standard controls.\nHowever, if you use:\nMouse.Move() (Hardware cursor simulation) Keyboard.Type() (Hardware input simulation) These might fail or behave erratically if the session is locked.\nBest Practice: Prefer Pattern methods over Input simulation.\nDon\u0026rsquo;t use: btn.Click() (which moves mouse and clicks). Use: btn.Invoke() (which uses the UIA InvokePattern). Invoke() works programmatically and doesn\u0026rsquo;t care if the mouse cursor is actually working or if the screen is locked.\nSummary # Running FlaUI on GitHub Actions is possible and powerful.\nUse windows-latest. Always capture screenshots on failure. Upload those screenshots as Artifacts. Prefer Invoke() patterns over physical mouse clicks to avoid session issues. Now your CI pipeline isn\u0026rsquo;t just compiling code—it\u0026rsquo;s proving your app actually works.\nFurther Reading # FlaUI on GitHub - Official FlaUI repository GitHub Actions Windows Runners - Documentation for Windows runners Upload Artifacts Action - Official action for artifact uploads UI Automation Patterns - Understanding invoke patterns ","date":"29 November 2025","externalUrl":null,"permalink":"/posts/flaui-github-actions-cicd/","section":"Posts","summary":"","title":"The CI/CD Challenge: Running FlaUI Tests in GitHub Actions","type":"posts"},{"content":"In a monolithic application, when a request is slow, you can fire up a profiler and find the bottleneck. In a microservices architecture, it\u0026rsquo;s not so simple. A single user request might travel through five, ten, or even more services before a response is returned.\nIf that request is slow, where is the problem? Is it Service A\u0026rsquo;s database query? Is it the network call from Service B to Service C? Is Service D waiting on an external API?\nThis is the problem that distributed tracing solves. It allows you to visualize the entire lifecycle of a request as it flows through your system, giving you a detailed breakdown of how much time was spent in each service.\nThe industry standard for implementing tracing is OpenTelemetry (OTel), a vendor-neutral, open-source observability framework.\nThe Core Concepts of Distributed Tracing # Trace: Represents the entire journey of a request. A trace is a collection of spans. Span: Represents a single unit of work within a trace, like an HTTP call, a database query, or a specific method execution. Spans have a start time, a duration, and can be nested. Trace Context: A set of unique identifiers (TraceId, SpanId) that are passed between services with each request (usually as HTTP headers). This context is what allows the system to stitch the individual spans together into a single, coherent trace. Implementing OpenTelemetry in .NET # Let\u0026rsquo;s imagine we have two services: an OrderService (an ASP.NET Core Web API) that receives the initial request, and a ProductService that it calls to get product details.\nStep 1: Add the NuGet Packages # You\u0026rsquo;ll need a few packages in both of your service projects.\n1 2 3 4 5 6 7 8 9 10 # Core OTel packages dotnet add package OpenTelemetry dotnet add package OpenTelemetry.Extensions.Hosting # Instrumentation for ASP.NET Core and HTTP calls dotnet add package OpenTelemetry.Instrumentation.AspNetCore dotnet add package OpenTelemetry.Instrumentation.Http # An exporter to send the data somewhere (we\u0026#39;ll use the console for this example) dotnet add package OpenTelemetry.Exporter.Console Other exporters are available for systems like Jaeger, Zipkin, or Application Insights.\nStep 2: Configure OpenTelemetry in Program.cs # In both OrderService and ProductService, you need to configure OpenTelemetry.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 using OpenTelemetry.Trace; using OpenTelemetry.Resources; var builder = WebApplication.CreateBuilder(args); var serviceName = \u0026#34;MyWebApp.OrderService\u0026#34;; // CHANGE THIS FOR EACH SERVICE builder.Services.AddOpenTelemetry() .ConfigureResource(resource =\u0026gt; resource.AddService(serviceName: serviceName)) .WithTracing(tracing =\u0026gt; tracing .AddAspNetCoreInstrumentation() // Automatically traces incoming ASP.NET Core requests .AddHttpClientInstrumentation() // Automatically traces outgoing HttpClient requests .AddSource(serviceName) // \u0026lt;--- Add this line to capture custom spans! .AddConsoleExporter()); // Sends traces to the console // You also need to add an HttpClient for the service to call others builder.Services.AddHttpClient(); // IMPORTANT: Register the ActivitySource so OTel knows to listen to it builder.Services.AddSingleton(new ActivitySource(serviceName)); var app = builder.Build(); // ... your app\u0026#39;s endpoints app.Run(); That\u0026rsquo;s the basic setup. With just this code, OpenTelemetry will automatically:\nStart a new trace and a root span for every incoming request to OrderService. When OrderService makes an HttpClient call to ProductService, OTel will automatically inject the trace context headers (traceparent, tracestate) into that outgoing request. When ProductService receives the request, it will see the trace context headers and continue the same trace, creating a new child span. Step 3: The Service Code # Let\u0026rsquo;s look at the code for the two services.\nOrderService\u0026rsquo;s Program.cs Endpoints:\n1 2 3 4 5 6 7 8 9 app.MapGet(\u0026#34;/order/{id}\u0026#34;, async (int id, IHttpClientFactory clientFactory) =\u0026gt; { var httpClient = clientFactory.CreateClient(); // OTel\u0026#39;s HttpClientInstrumentation will automatically add trace headers here var productInfo = await httpClient.GetStringAsync($\u0026#34;http://localhost:5001/product/123\u0026#34;); // Assuming ProductService runs on port 5001 return $\u0026#34;Order {id} contains: {productInfo}\u0026#34;; }); ProductService\u0026rsquo;s Program.cs Endpoints:\n1 2 3 4 5 6 7 8 9 10 // OTel configuration is the same, just with a different service name // ... app.MapGet(\u0026#34;/product/{id}\u0026#34;, async (int id) =\u0026gt; { // OTel\u0026#39;s AspNetCoreInstrumentation has already started a child span // for this incoming request. await Task.Delay(150); // Simulate some work return $\u0026#34;Product {id} - \u0026#39;Super Widget\u0026#39;\u0026#34;; }); Step 4: Run and Observe # When you run both services and make a request to http://localhost:5000/order/1, you\u0026rsquo;ll see output in the console of both applications from the ConsoleExporter.\nOrderService Console Output:\n1 2 3 4 5 6 7 Activity.TraceId: a1b2c3d4... Activity.SpanId: e5f6g7h8... Activity.DisplayName: /order/{id} Activity.StartTimeUtc: ... Activity.Duration: 00:00:00.200 Resource associated with Activity: service.name: MyWebApp.OrderService This is the root span.\nProductService Console Output:\n1 2 3 4 5 6 7 8 Activity.TraceId: a1b2c3d4... \u0026lt;-- SAME TRACE ID Activity.ParentSpanId: e5f6g7h8... \u0026lt;-- Parent is the OrderService span Activity.SpanId: i9j0k1l2... Activity.DisplayName: /product/{id} Activity.StartTimeUtc: ... Activity.Duration: 00:00:00.150 Resource associated with Activity: service.name: MyWebApp.ProductService This is the child span. A backend like Jaeger or Zipkin would visualize this as a waterfall diagram, showing that the OrderService span took 200ms, and 150ms of that was spent waiting for the ProductService span to complete.\nAdding Custom Spans # Automatic instrumentation is great, but sometimes you want to trace a specific piece of work inside a method. You can create custom spans for this.\nFirst, you need to define an ActivitySource. A common pattern is to hold this in a static field or inject it.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System.Diagnostics; // At the top of your class or in a shared location public static readonly ActivitySource MyActivitySource = new ActivitySource(\u0026#34;MyWebApp.OrderService\u0026#34;); // Inside your endpoint app.MapGet(\u0026#34;/order/{id}\u0026#34;, async (int id, IHttpClientFactory clientFactory) =\u0026gt; { // Create a custom span using (var activity = MyActivitySource.StartActivity(\u0026#34;CalculatingTax\u0026#34;)) { // Add custom attributes (called \u0026#34;tags\u0026#34;) to your span activity?.SetTag(\u0026#34;order.id\u0026#34;, id); await Task.Delay(50); // Simulate tax calculation } // ... rest of the code Crucial Step: For these custom spans to appear in your trace, you must tell the OpenTelemetry builder to listen to this specific ActivitySource. I\u0026rsquo;ve updated the configuration code above to include .AddSource(serviceName). Without that line, StartActivity returns null!\nConclusion # Distributed tracing is no longer a \u0026ldquo;nice-to-have\u0026rdquo;—it\u0026rsquo;s a necessity for understanding and debugging modern, distributed systems. OpenTelemetry provides a powerful, standardized, and easy-to-use framework for adding this capability to your .NET applications. With just a few lines of configuration, you can gain deep insights into the performance of your entire system, making it faster and more reliable.\nFurther Reading # OpenTelemetry .NET Documentation - Official OpenTelemetry documentation .NET Observability with OpenTelemetry - Microsoft\u0026rsquo;s guide Jaeger Documentation - Learn about the Jaeger tracing backend OpenTelemetry on GitHub - The .NET SDK repository ","date":"18 November 2025","externalUrl":null,"permalink":"/posts/distributed-tracing-in-dotnet-with-opentelemetry/","section":"Posts","summary":"","title":"Distributed Tracing in .NET with OpenTelemetry","type":"posts"},{"content":"","date":"18 November 2025","externalUrl":null,"permalink":"/tags/microservices/","section":"Tags","summary":"","title":"Microservices","type":"tags"},{"content":"","date":"18 November 2025","externalUrl":null,"permalink":"/categories/observability/","section":"Categories","summary":"","title":"Observability","type":"categories"},{"content":"","date":"18 November 2025","externalUrl":null,"permalink":"/tags/observability/","section":"Tags","summary":"","title":"Observability","type":"tags"},{"content":"","date":"18 November 2025","externalUrl":null,"permalink":"/tags/opentelemetry/","section":"Tags","summary":"","title":"OpenTelemetry","type":"tags"},{"content":"When building AI-powered applications, developers often face a difficult choice: run models locally for speed and privacy, or use powerful cloud-based models for state-of-the-art performance?\nThe answer is often both.\nA hybrid AI architecture combines the best of both worlds. It uses small, efficient local models for routine tasks and intelligently escalates to larger, more capable cloud models when necessary. This pattern is perfect for creating responsive, cost-effective, and powerful AI experiences in .NET applications.\nThe Hybrid AI Pattern # Imagine a customer support chatbot in a desktop application. The goal is to answer user queries quickly and cheaply, while still being able to handle complex questions.\nStep 1: Local First (The \u0026ldquo;Fast Path\u0026rdquo;)\nThe user asks a question. The application first uses a local, lightweight model (e.g., an ONNX-based sentence-transformer) to perform a semantic search over a local FAQ database. If a high-confidence match is found, the answer is provided instantly. This is fast, free (no API calls), and completely private. Step 2: Cloud Escalation (The \u0026ldquo;Smart Path\u0026rdquo;)\nIf the local search returns a low-confidence result, the application escalates the query to a powerful cloud-based LLM like GPT-4. The LLM receives the user\u0026rsquo;s question along with the context retrieved from the local search. The LLM generates a more nuanced and comprehensive answer. This approach ensures that simple, common questions are handled efficiently, while complex, novel queries get the attention of a more powerful reasoning engine.\nThe Tools # .NET 8+ ONNX Runtime: For running the local semantic search model. A Vector Store: To hold the FAQ embeddings. An LLM SDK: Such as Azure.AI.OpenAI to connect to cloud models. Example: The Router Logic # The core of a hybrid system is the \u0026ldquo;router\u0026rdquo;—a piece of logic that decides whether to use the local model or escalate to the cloud.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 using Azure; using Azure.AI.OpenAI; using System.Linq; public class HybridAIService { private readonly LocalSearchService _localSearch; private readonly OpenAIClient _cloudClient; private const double ConfidenceThreshold = 0.85; public HybridAIService(string onnxModelPath, string openAIKey) { _localSearch = new LocalSearchService(onnxModelPath); _cloudClient = new OpenAIClient(openAIKey); } public async Task\u0026lt;string\u0026gt; GetAnswerAsync(string userQuestion) { // 1. Try the local-first approach var localResult = _localSearch.FindBestMatch(userQuestion); // 2. The Router Logic if (localResult.Confidence \u0026gt;= ConfidenceThreshold) { Console.WriteLine(\u0026#34;INFO: Answered using local model.\u0026#34;); return localResult.Answer; } else { Console.WriteLine(\u0026#34;INFO: Low local confidence. Escalating to cloud model.\u0026#34;); // 3. Escalate to the cloud return await GetCloudAnswerAsync(userQuestion, localResult.Answer); } } private async Task\u0026lt;string\u0026gt; GetCloudAnswerAsync(string question, string retrievedContext) { var prompt = $@\u0026#34; Based on the following context, please provide a comprehensive answer to the user\u0026#39;s question. If the context is not sufficient, use your general knowledge. Context: \u0026#34;\u0026#34;{retrievedContext}\u0026#34;\u0026#34; Question: \u0026#34;\u0026#34;{question}\u0026#34;\u0026#34; \u0026#34;; var chatCompletionsOptions = new ChatCompletionsOptions() { DeploymentName = \u0026#34;gpt-4\u0026#34;, // Or your preferred model Messages = { new ChatRequestSystemMessage(\u0026#34;You are a helpful assistant.\u0026#34;), new ChatRequestUserMessage(prompt), } }; // Use the modern Azure.AI.OpenAI v2.0+ client patterns if available, // but for clarity here we stick to the standard ChatCompletions pattern. Response\u0026lt;ChatCompletions\u0026gt; response = await _cloudClient.GetChatCompletionsAsync(chatCompletionsOptions); return response.Value.Choices.First().Message.Content; } } // Dummy implementation for LocalSearchService to make the code compile public class LocalSearchService { public LocalSearchService(string model) { } // Returns a tuple with named elements for clarity public (string Answer, double Confidence) FindBestMatch(string query) { // In a real app, this would perform a vector search using ONNX Runtime or a local vector DB. // For this example, we simulate a match. if (query.Contains(\u0026#34;password reset\u0026#34;, StringComparison.OrdinalIgnoreCase)) { return (\u0026#34;You can reset your password in the Account settings.\u0026#34;, 0.9); } return (\u0026#34;Sorry, I\u0026#39;m not sure how to help with that.\u0026#34;, 0.5); } } How the Router Works: # Confidence Score: The local semantic search doesn\u0026rsquo;t just return an answer; it returns a confidence score (e.g., the cosine similarity of the vectors). Threshold: We define a ConfidenceThreshold. If the score is above this value, we trust the local answer. Escalation: If the score is below the threshold, we make the API call to the cloud model, passing the best local result as context to help the LLM. Benefits of the Hybrid Approach # Responsiveness: Users get instant answers for common questions, improving the user experience. Cost-Effectiveness: Reduces the number of expensive API calls to cloud services. You only pay for the queries that truly require advanced reasoning. Privacy: Sensitive data can be processed locally without ever leaving the user\u0026rsquo;s machine. Only the escalated, low-confidence queries are sent to the cloud. Offline Capability: The \u0026ldquo;fast path\u0026rdquo; can work even when the user is offline, providing a baseline level of functionality. Conclusion # The hybrid AI pattern offers a pragmatic and powerful way to build intelligent .NET applications. By combining the strengths of local and cloud models, you can create systems that are fast, efficient, private, and smart. Instead of choosing between local or cloud, choose the architecture that lets you use the right tool for the right job.\nFurther Reading # Semantic Kernel Documentation - Build AI applications with .NET OpenAI API Documentation - Cloud AI service Ollama Documentation - Local AI models Hybrid AI Architecture Patterns - Azure Architecture guide ","date":"2 November 2025","externalUrl":null,"permalink":"/posts/hybrid-ai-combining-local-and-cloud-models-in-dotnet/","section":"Posts","summary":"","title":"Hybrid AI: Combining Local and Cloud Models in .NET","type":"posts"},{"content":"","date":"2 November 2025","externalUrl":null,"permalink":"/tags/onnx/","section":"Tags","summary":"","title":"ONNX","type":"tags"},{"content":"You\u0026rsquo;ve built a brilliant AI-powered application in .NET, perhaps using an ONNX model for local inference. It works perfectly on your machine. Now, how do you ship it?\nThe answer is Docker. Containerization solves the \u0026ldquo;it works on my machine\u0026rdquo; problem by packaging your application, its dependencies, your .NET runtime, and even your AI models into a single, isolated unit called an image.\nThis guide will walk you through creating an optimized, multi-stage Dockerfile for a .NET application that uses the ONNX Runtime.\nWhy Docker for AI Apps? # Environment Consistency: The exact same environment is used for development, testing, and production. No more \u0026ldquo;missing dependency\u0026rdquo; errors. Dependency Isolation: Your app\u0026rsquo;s specific version of CUDA, ONNX Runtime, or any other library won\u0026rsquo;t conflict with other applications on the host machine. Scalability: Docker containers can be easily scaled up or down using orchestrators like Kubernetes. Portability: A Docker image built on a Windows machine will run flawlessly on a Linux server in the cloud. The Project Structure # Let\u0026rsquo;s assume we have a simple console application with the following structure:\n1 2 3 4 5 6 /MyNetAiApp |-- MyNetAiApp.csproj |-- Program.cs |-- model/ | |-- sentiment-model.onnx |-- Dockerfile The sentiment-model.onnx file should be marked as Content and set to Copy if newer in the .csproj file:\n1 2 3 4 5 \u0026lt;ItemGroup\u0026gt; \u0026lt;Content Include=\u0026#34;model\\sentiment-model.onnx\u0026#34;\u0026gt; \u0026lt;CopyToOutputDirectory\u0026gt;PreserveNewest\u0026lt;/CopyToOutputDirectory\u0026gt; \u0026lt;/Content\u0026gt; \u0026lt;/ItemGroup\u0026gt; The Multi-Stage Dockerfile # A multi-stage build is the best practice for creating lean, secure Docker images. We use one stage (the build stage) to compile the application with the full SDK, and a final stage that only contains the minimal runtime and our application artifacts.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # Stage 1: The Build Stage # We use the .NET SDK image which contains all the tools needed to build the app FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /source # Copy the project file and restore dependencies first # This leverages Docker\u0026#39;s layer caching. As long as the .csproj doesn\u0026#39;t change, # this layer won\u0026#39;t be re-run, speeding up subsequent builds. COPY *.csproj . RUN dotnet restore # Copy the rest of the source code and build the application COPY . . RUN dotnet publish -c Release -o /app --no-restore # Stage 2: The Final Stage # We use the much smaller ASP.NET runtime image. For console apps, you can even use # mcr.microsoft.com/dotnet/runtime:8.0 if you don\u0026#39;t need any web-related libraries. FROM mcr.microsoft.com/dotnet/runtime:8.0 WORKDIR /app # CRITICAL: Install libgomp1. # The ONNX Runtime on Linux depends on OpenMP, which isn\u0026#39;t included in the # default .NET runtime image. Without this, your app will crash with a # \u0026#34;DllNotFoundException\u0026#34; for onnxruntime. RUN apt-get update \u0026amp;\u0026amp; apt-get install -y --no-install-recommends libgomp1 \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* # Copy the published application from the build stage COPY --from=build /app . # Set the entry point of the container ENTRYPOINT [\u0026#34;dotnet\u0026#34;, \u0026#34;MyNetAiApp.dll\u0026#34;] Key Optimizations in This Dockerfile: # Multi-Stage Build: The final image doesn\u0026rsquo;t contain the .NET SDK (which is huge). It only contains the minimal runtime needed to execute the application. Layer Caching: By copying the .csproj file and running dotnet restore before copying the rest of the code, we ensure that Docker doesn\u0026rsquo;t need to re-download all the NuGet packages every time we change a single line of C# code. Missing Dependencies: We explicitly install libgomp1. This is a classic \u0026ldquo;gotcha\u0026rdquo; when moving .NET AI apps from Windows to Linux containers. Handling Large Models with .dockerignore # If your AI models are massive (e.g., 5GB+), you don\u0026rsquo;t want to copy them into the build stage context if they aren\u0026rsquo;t needed for compilation.\nCreate a .dockerignore file to exclude heavy assets from the initial build context copy, and then copy them explicitly only where needed.\n1 2 3 4 # .dockerignore bin/ obj/ model/ Then, in your Dockerfile, copy the model directly to the final stage:\n1 2 # In Stage 2 COPY model/ /app/model/ Handling GPU-Enabled Models # Running AI on a CPU is fine for simple tasks, but for heavy lifting, you need a GPU. This complicates things because the standard .NET images don\u0026rsquo;t include NVIDIA drivers or CUDA libraries.\nTo support GPUs, you typically need to:\nUse an NVIDIA CUDA base image (e.g., nvidia/cuda:12.1-base-ubuntu22.04). Install the .NET Runtime on top of it. Use the Microsoft.ML.OnnxRuntime.Gpu NuGet package instead of the CPU version. 1 2 3 4 5 6 7 8 9 # Example: Using a CUDA base image and installing .NET FROM nvidia/cuda:12.1-base-ubuntu22.04 # Install .NET Runtime (simplified for brevity) RUN apt-get update \u0026amp;\u0026amp; apt-get install -y dotnet-runtime-8.0 WORKDIR /app COPY --from=build /app . ENTRYPOINT [\u0026#34;dotnet\u0026#34;, \u0026#34;MyNetAiApp.dll\u0026#34;] When running the container, you must use the --gpus all flag:\n1 docker run --rm --gpus all my-net-ai-app-gpu Conclusion # Docker is an essential tool for modern application deployment, and it\u0026rsquo;s a perfect match for .NET AI applications. By using a multi-stage Dockerfile, you can create lean, portable, and scalable images that encapsulate your application, your models, and all their dependencies. This consistent and reproducible approach simplifies deployment from your local machine to any cloud provider.\nFurther Reading # Docker Documentation - Official Docker documentation .NET Docker Images - Official .NET container images ONNX Runtime Docker Images - Pre-built ONNX Runtime containers Multi-stage builds - Docker\u0026rsquo;s guide on multi-stage builds ","date":"19 October 2025","externalUrl":null,"permalink":"/posts/deploying-dotnet-ai-applications-with-docker/","section":"Posts","summary":"","title":"Deploying .NET AI Applications with Docker","type":"posts"},{"content":"","date":"19 October 2025","externalUrl":null,"permalink":"/tags/deployment/","section":"Tags","summary":"","title":"Deployment","type":"tags"},{"content":"","date":"12 October 2025","externalUrl":null,"permalink":"/tags/advanced/","section":"Tags","summary":"","title":"Advanced","type":"tags"},{"content":"","date":"12 October 2025","externalUrl":null,"permalink":"/tags/expression-trees/","section":"Tags","summary":"","title":"Expression Trees","type":"tags"},{"content":"Have you ever wondered how Entity Framework translates your C# LINQ queries into SQL?\n1 var users = db.Users.Where(u =\u0026gt; u.IsActive \u0026amp;\u0026amp; u.RegistrationDate \u0026gt; lastYear); The magic behind this is a powerful and often misunderstood feature of .NET: Expression Trees.\nAn Expression Tree is a data structure that represents code. Instead of compiling your lambda expression into executable IL (Intermediate Language), the compiler can transform it into a tree-like object model. Every node in the tree represents a piece of the code: a binary operation, a method call, a property access, etc.\nIn short, an expression tree turns your code into data.\nFunc\u0026lt;T\u0026gt; vs. Expression\u0026lt;Func\u0026lt;T\u0026gt;\u0026gt; # The key to understanding expression trees is the difference between these two types:\nFunc\u0026lt;User, bool\u0026gt; myFunc = u =\u0026gt; u.IsActive;\nThis is a standard delegate. The lambda u =\u0026gt; u.IsActive is compiled into executable code (IL) that your program can run directly. You can call it: myFunc(someUser). You cannot inspect it. You don\u0026rsquo;t know how it determines if a user is active. Expression\u0026lt;Func\u0026lt;User, bool\u0026gt;\u0026gt; myExpr = u =\u0026gt; u.IsActive;\nThis is an expression tree. The lambda is not compiled into executable code. Instead, it\u0026rsquo;s converted into a tree of objects representing the logic. You can inspect it. You can walk the tree and see that it\u0026rsquo;s a property access on the IsActive member of the User object. You can compile and run it if you want to: myExpr.Compile()(someUser). By wrapping your lambda in Expression\u0026lt;\u0026gt;, you give the compiler permission to treat it as data.\nDeconstructing an Expression Tree # Let\u0026rsquo;s look at a slightly more complex expression and see how it\u0026rsquo;s represented. First, let\u0026rsquo;s define a simple model to work with:\n1 2 3 4 5 public class User { public bool IsActive { get; set; } public int FollowerCount { get; set; } } Now, let\u0026rsquo;s inspect an expression that filters these users:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 using System.Linq.Expressions; Expression\u0026lt;Func\u0026lt;User, bool\u0026gt;\u0026gt; filter = u =\u0026gt; u.IsActive \u0026amp;\u0026amp; u.FollowerCount \u0026gt; 100; // The root of the tree is the binary \u0026#39;AndAlso\u0026#39; operation (\u0026amp;\u0026amp;) var andExpression = (BinaryExpression)filter.Body; Console.WriteLine($\u0026#34;Operation: {andExpression.NodeType}\u0026#34;); // AndAlso // The left side of the \u0026#39;\u0026amp;\u0026amp;\u0026#39; is the property access \u0026#39;u.IsActive\u0026#39; var left = (MemberExpression)andExpression.Left; Console.WriteLine($\u0026#34;Left: Property \u0026#39;{left.Member.Name}\u0026#39;\u0026#34;); // Left: Property \u0026#39;IsActive\u0026#39; // The right side is the \u0026#39;\u0026gt;\u0026#39; comparison var right = (BinaryExpression)andExpression.Right; Console.WriteLine($\u0026#34;Right Operation: {right.NodeType}\u0026#34;); // GreaterThan // The right side\u0026#39;s left part is the property access \u0026#39;u.FollowerCount\u0026#39; var rightLeft = (MemberExpression)right.Left; Console.WriteLine($\u0026#34;Right\u0026#39;s Left: Property \u0026#39;{rightLeft.Member.Name}\u0026#39;\u0026#34;); // Right\u0026#39;s Left: Property \u0026#39;FollowerCount\u0026#39; // The right side\u0026#39;s right part is the constant value 100 var rightRight = (ConstantExpression)right.Right; Console.WriteLine($\u0026#34;Right\u0026#39;s Right: Value \u0026#39;{rightRight.Value}\u0026#39;\u0026#34;); // Right\u0026#39;s Right: Value \u0026#39;100\u0026#39; As you can see, we can programmatically walk the entire lambda and understand its intent.\nSo What\u0026rsquo;s the Point? # This ability to inspect code as data is what enables some of the most powerful libraries in .NET:\nObject-Relational Mappers (ORMs) like Entity Framework:\nEF takes your Expression\u0026lt;Func\u0026lt;User, bool\u0026gt;\u0026gt; from a .Where() call. It walks the expression tree. When it sees AndAlso, it writes \u0026quot;AND\u0026quot;. When it sees u.IsActive, it writes \u0026quot;[IsActive] = 1\u0026quot;. When it sees u.FollowerCount \u0026gt; 100, it writes \u0026quot;[FollowerCount] \u0026gt; 100\u0026quot;. By translating the tree, it builds a SQL query string that can be executed by the database. Dynamic Querying:\nYou can build expression trees manually at runtime. This allows you to construct complex, type-safe queries based on user input or configuration without resorting to string concatenation (which is vulnerable to SQL injection). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 1. Define the parameter \u0026#39;u\u0026#39; (as in \u0026#39;u =\u0026gt; ...\u0026#39;) var userParam = Expression.Parameter(typeof(User), \u0026#34;u\u0026#34;); // 2. Create expressions for property access var isActiveProp = Expression.Property(userParam, nameof(User.IsActive)); var followerProp = Expression.Property(userParam, nameof(User.FollowerCount)); // 3. Create constant values to compare against var trueConst = Expression.Constant(true); var numConst = Expression.Constant(100); // 4. Build the comparison logic // u.IsActive == true var isActiveCheck = Expression.Equal(isActiveProp, trueConst); // u.FollowerCount \u0026gt; 100 var followerCheck = Expression.GreaterThan(followerProp, numConst); // 5. Combine them with AND (\u0026amp;\u0026amp;) var combined = Expression.AndAlso(isActiveCheck, followerCheck); // 6. Compile the final lambda var lambda = Expression.Lambda\u0026lt;Func\u0026lt;User, bool\u0026gt;\u0026gt;(combined, userParam); // Result: u =\u0026gt; (u.IsActive == True) AndAlso (u.FollowerCount \u0026gt; 100) Mapping Libraries like AutoMapper:\nAutoMapper can use expression trees to generate highly optimized mapping functions between two types at runtime, avoiding the performance overhead of reflection on every call. Conclusion # Expression Trees are a C# \u0026ldquo;power user\u0026rdquo; feature. While you may not write them every day, understanding them is crucial to grasping how many modern .NET libraries work under the hood. They are the bridge that allows your C# code to be understood and translated into other languages, like SQL, or to be dynamically constructed and executed at runtime. The next time you write a LINQ to SQL query, take a moment to appreciate the elegant data structure that is working behind the scenes.\nFurther Reading # Expression Trees - C# Programming Guide - Official Microsoft documentation Expression Class API Reference - Detailed API documentation Building Query Providers with LINQ - Deep dive into LINQ providers ","date":"12 October 2025","externalUrl":null,"permalink":"/posts/expression-trees-in-csharp-beyond-the-basics/","section":"Posts","summary":"","title":"Expression Trees in C#: Beyond the Basics","type":"posts"},{"content":"","date":"12 October 2025","externalUrl":null,"permalink":"/tags/reflection/","section":"Tags","summary":"","title":"Reflection","type":"tags"},{"content":"","date":"5 October 2025","externalUrl":null,"permalink":"/tags/langchain/","section":"Tags","summary":"","title":"LangChain","type":"tags"},{"content":"","date":"5 October 2025","externalUrl":null,"permalink":"/tags/orchestration/","section":"Tags","summary":"","title":"Orchestration","type":"tags"},{"content":"When building applications on top of Large Language Models (LLMs), you quickly realize you need more than just API calls. You need a way to chain prompts, connect to data sources, and orchestrate complex workflows. This is where AI orchestration frameworks come in.\nFor a long time, the dominant player has been LangChain, a powerful, Python-first library. But Microsoft has entered the scene with Semantic Kernel, a .NET-native solution designed to be lightweight, extensible, and enterprise-ready.\nSo, which one should a .NET developer choose?\nNote: Both libraries are evolving rapidly. The code examples below reflect the state of the libraries as of late 2025. Always check the official documentation for the latest breaking changes.\nPhilosophy and Design # LangChain:\nKitchen Sink Included: LangChain aims to be an all-encompassing framework. It has a vast number of integrations for models, vector stores, and tools right out of the box. Convention over Configuration: It often provides high-level, \u0026ldquo;magical\u0026rdquo; chains that perform complex tasks with minimal code. This is great for rapid prototyping. Python-Centric: While it has a JavaScript/TypeScript port, its primary development and community are in the Python ecosystem. The .NET port, LangChain.NET, is a community-driven effort and often lags behind the Python version in features. Semantic Kernel:\nMinimalist Core: Semantic Kernel starts with a small, elegant core. You, the developer, plug in the components you need (e.g., a connector for OpenAI, another for ChromaDB). Configuration over Convention: It forces a more deliberate and structured approach. You explicitly define your plugins, memory, and connectors. This can mean more boilerplate but leads to more maintainable code. .NET First: It\u0026rsquo;s designed from the ground up for the .NET ecosystem, with idiomatic C# and full support from Microsoft. Feature Comparison # Feature LangChain (.NET Port) Semantic Kernel Core Idea Chains: Linking components together. Plugins: Encapsulating prompts and native functions. Prompting String templates with {variable} syntax. SK Prompts (.skprompt files) with Handlebars {{$input}} syntax. Memory Abstract concept of Memory for storing chat history. A more robust SemanticMemory for both context and RAG. Extensibility Good, but relies on community ports for .NET. Excellent. The design is based on dependency injection. Tooling Fewer pre-built tools in the .NET version. Growing ecosystem of connectors; seamless C# integration. Maturity The Python version is very mature; .NET is less so. Backed by Microsoft, evolving rapidly and stabilizing. Code Example: A Simple RAG Chain # Let\u0026rsquo;s see how both frameworks would implement a simple RAG (Retrieval-Augmented Generation) task: answer a question using a piece of context.\nLangChain.NET Example # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using LangChain.Chains.LLM; using LangChain.Prompts; using LangChain.Providers.OpenAI; // Setup var llm = new Gpt35Turbo(Environment.GetEnvironmentVariable(\u0026#34;OPENAI_API_KEY\u0026#34;)); var template = \u0026#34;Context: {context}\\n\\nQuestion: {question}\\n\\nAnswer:\u0026#34;; var prompt = new PromptTemplate(template, new[] { \u0026#34;context\u0026#34;, \u0026#34;question\u0026#34; }); var chain = new LlmChain(llm, prompt); // Execution var context = \u0026#34;The .NET Framework was first released in 2002.\u0026#34;; var question = \u0026#34;When was .NET released?\u0026#34;; var result = await chain.CallAsync(new Dictionary\u0026lt;string, object\u0026gt; { { \u0026#34;context\u0026#34;, context }, { \u0026#34;question\u0026#34;, question } }); Console.WriteLine(result.Value[\u0026#34;text\u0026#34;]); // Expected Output: The .NET Framework was first released in 2002. LangChain\u0026rsquo;s approach is direct and focuses on chaining the prompt and model together quickly.\nSemantic Kernel Example (v1.0+) # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; // Setup var builder = Kernel.CreateBuilder(); builder.AddOpenAIChatCompletion( modelId: \u0026#34;gpt-3.5-turbo\u0026#34;, apiKey: Environment.GetEnvironmentVariable(\u0026#34;OPENAI_API_KEY\u0026#34;)); var kernel = builder.Build(); var prompt = @\u0026#34; Context: {{$context}} Question: {{$question}} Answer:\u0026#34;; // Create the function from the prompt var function = kernel.CreateFunctionFromPrompt(prompt); // Execution var context = \u0026#34;The .NET Framework was first released in 2002.\u0026#34;; var question = \u0026#34;When was .NET released?\u0026#34;; var arguments = new KernelArguments { [\u0026#34;context\u0026#34;] = context, [\u0026#34;question\u0026#34;] = question }; var result = await kernel.InvokeAsync(function, arguments); Console.WriteLine(result.GetValue\u0026lt;string\u0026gt;()); // Expected Output: The .NET Framework was first released in 2002. Semantic Kernel\u0026rsquo;s approach is more structured. Notice the use of KernelArguments and InvokeAsync, which are part of the modern v1.0 API. It revolves around the Kernel as the central orchestrator.\nWhen to Choose Which? # Choose LangChain.NET if:\nYou are rapidly prototyping and want access to a vast library of pre-built components (and are willing to accept that the .NET version may not have them all). You are coming from a Python background and want a familiar experience. Your project is small and less focused on long-term maintainability. Choose Semantic Kernel if:\nYou are building a production-ready, enterprise-grade application in .NET. You value clean, testable, and maintainable code. You want a framework that feels like idiomatic C# and integrates well with concepts like dependency injection. You prefer a \u0026ldquo;bring your own dependencies\u0026rdquo; model over a monolithic framework. Conclusion # For the majority of .NET developers, Semantic Kernel is the superior choice. It\u0026rsquo;s built for the ecosystem you already know and love. Its design philosophy, while requiring a bit more initial setup, pays dividends in the long run by promoting a more robust and maintainable architecture. LangChain is a fantastic tool for exploration and rapid development, but Semantic Kernel is the framework you\u0026rsquo;ll want to build your business on.\nFurther Reading # Semantic Kernel Documentation - Official Microsoft documentation Semantic Kernel on GitHub - Source code and examples LangChain Documentation - Official LangChain docs (Python-focused) LangChain.NET on GitHub - The .NET port ","date":"5 October 2025","externalUrl":null,"permalink":"/posts/semantic-kernel-vs-langchain-a-dotnet-developers-guide/","section":"Posts","summary":"","title":"Semantic Kernel vs. LangChain: A .NET Developer's Guide","type":"posts"},{"content":"","date":"1 October 2025","externalUrl":null,"permalink":"/tags/memory-management/","section":"Tags","summary":"","title":"Memory Management","type":"tags"},{"content":"","date":"1 October 2025","externalUrl":null,"permalink":"/categories/performance/","section":"Categories","summary":"","title":"Performance","type":"categories"},{"content":"","date":"1 October 2025","externalUrl":null,"permalink":"/tags/performance/","section":"Tags","summary":"","title":"Performance","type":"tags"},{"content":"For years, high-performance .NET code meant fighting the garbage collector (GC). Every time you sliced a string, took a substring, or passed a portion of an array, you were likely allocating new memory. These small allocations add up, putting pressure on the GC and hurting your application\u0026rsquo;s throughput.\nEnter Span\u0026lt;T\u0026gt; and Memory\u0026lt;T\u0026gt;, two revolutionary types that provide a unified and allocation-free way to work with contiguous memory. Understanding them is key to writing modern, high-performance C# code.\nThe Problem: Allocations Everywhere # Let\u0026rsquo;s look at a classic example: parsing a full name from a string.\n1 2 3 4 5 6 7 8 9 10 public (string, string) GetFirstAndLastName(string fullName) { // 1. Allocates a string[] array // 2. Allocates new string objects for \u0026#34;First\u0026#34; and \u0026#34;Last\u0026#34; var parts = fullName.Split(\u0026#39; \u0026#39;); var firstName = parts[0]; var lastName = parts[1]; return (firstName, lastName); } The call to fullName.Split(' ') is the problem. It allocates a new array of strings (string[]) on the heap, plus a new string object for every part it finds. If you call this method in a tight loop, you\u0026rsquo;ll be creating a massive amount of garbage for the GC to collect.\nWhat if we could just represent a \u0026ldquo;view\u0026rdquo; or a \u0026ldquo;slice\u0026rdquo; of the original string without allocating anything new?\nSpan\u0026lt;T\u0026gt;: The Allocation-Free Slice # Span\u0026lt;T\u0026gt; is a ref struct that represents a contiguous block of memory. It\u0026rsquo;s like a pointer, but safe and managed. It can point to memory on the stack, in a managed array, or even to unmanaged memory.\nBecause it\u0026rsquo;s a ref struct, it has some important limitations:\nIt can only live on the stack. It cannot be a field in a regular class or struct (only in other ref structs). It cannot be used in async methods across an await boundary. It cannot be boxed or assigned to object. These rules ensure that Span\u0026lt;T\u0026gt; can\u0026rsquo;t outlive the memory it\u0026rsquo;s pointing to, preventing memory corruption.\nThe Power of stackalloc # One of the best features of Span\u0026lt;T\u0026gt; is that it allows you to use stack memory safely without the unsafe keyword.\n1 2 3 4 // Create a small buffer on the stack (no GC overhead!) Span\u0026lt;byte\u0026gt; buffer = stackalloc byte[128]; // Use it just like an array buffer[0] = 42; Rewriting GetFirstAndLastName with Span\u0026lt;T\u0026gt; # Let\u0026rsquo;s rewrite our name-parsing method using ReadOnlySpan\u0026lt;char\u0026gt; (the Span\u0026lt;T\u0026gt; equivalent for immutable strings).\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // Note the \u0026#39;ReadOnly\u0026#39; prefix, as strings are immutable. public (ReadOnlySpan\u0026lt;char\u0026gt;, ReadOnlySpan\u0026lt;char\u0026gt;) GetFirstAndLastName_Span(ReadOnlySpan\u0026lt;char\u0026gt; fullName) { var spaceIndex = fullName.IndexOf(\u0026#39; \u0026#39;); if (spaceIndex == -1) { // Return the whole name as the first name, and an empty span for the last. return (fullName, ReadOnlySpan\u0026lt;char\u0026gt;.Empty); } // Use C# Range syntax for cleaner slicing var firstName = fullName[..spaceIndex]; var lastName = fullName[(spaceIndex + 1)..]; return (firstName, lastName); } Zero allocations.\nThe range operator ([..]) doesn\u0026rsquo;t create a new string. It simply creates a new ReadOnlySpan\u0026lt;char\u0026gt; that points to the same underlying memory as the original string.\nMemory\u0026lt;T\u0026gt;: The Heap-Friendly Cousin # The stack-only limitation of Span\u0026lt;T\u0026gt; is a deal-breaker for many scenarios, especially in async code. This is where Memory\u0026lt;T\u0026gt; comes in.\nMemory\u0026lt;T\u0026gt; is a regular struct that can live on the heap. It serves as a \u0026ldquo;handle\u0026rdquo; to a block of memory. It\u0026rsquo;s slightly less performant than Span\u0026lt;T\u0026gt; because it has an extra layer of indirection, but it doesn\u0026rsquo;t have the same restrictions.\nThe magic is in the relationship between them: you can get a Span\u0026lt;T\u0026gt; from a Memory\u0026lt;T\u0026gt; at any time.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public async Task ProcessDataAsync(Memory\u0026lt;byte\u0026gt; buffer) { // \u0026#39;buffer\u0026#39; can be stored in a class, passed around, and used across awaits. await Task.Delay(100); // Simulate async work // When you\u0026#39;re ready to do the actual, high-performance processing, // you get a Span from the Memory. Span\u0026lt;byte\u0026gt; span = buffer.Span; // Now you can work with the data allocation-free in this synchronous context. for (int i = 0; i \u0026lt; span.Length; i++) { span[i] *= 2; } } The Golden Rule # Pass Memory\u0026lt;T\u0026gt; around: Use it for your class fields, method parameters (especially for async methods), and any time you need to store a reference to a buffer on the heap. Process with Span\u0026lt;T\u0026gt;: When you are inside a synchronous method and ready to do the actual work, get a .Span from your Memory\u0026lt;T\u0026gt; and operate on that. Practical Use Cases # High-Performance Parsing: Parsing text, binary data, or network protocols without creating intermediate strings or byte arrays. Image and Audio Processing: Working directly on raw pixel or audio data. API Design: Libraries like ASP.NET Core and System.Text.Json use Span\u0026lt;T\u0026gt; and Memory\u0026lt;T\u0026gt; extensively in their internals to reduce allocations and improve throughput. For example, you can read a request body directly into a Memory\u0026lt;byte\u0026gt; buffer. Conclusion # Span\u0026lt;T\u0026gt; and Memory\u0026lt;T\u0026gt; are not just for library authors. They are powerful tools for any .NET developer looking to optimize their code. By thinking in terms of memory slices instead of object allocations, you can significantly reduce the pressure on the garbage collector, leading to faster, more efficient, and more scalable applications. If you\u0026rsquo;re not using them yet, it\u0026rsquo;s time to start.\nFurther Reading # Span Documentation - Official Microsoft documentation Memory and Span usage guidelines - Best practices C# 7.2 - Span and ref - Language reference ","date":"1 October 2025","externalUrl":null,"permalink":"/posts/the-power-of-span-and-memory-in-dotnet/","section":"Posts","summary":"","title":"The Power of Span\u003cT\u003e and Memory\u003cT\u003e in .NET","type":"posts"},{"content":"Large Language Models (LLMs) are powerful, but they have a critical limitation: their knowledge is frozen at the time of training. They don\u0026rsquo;t know about your private documents or recent events. The solution is Retrieval-Augmented Generation (RAG), a technique that enhances LLM responses with information retrieved from your own data sources.\nWhile many RAG systems rely on Python and heavy frameworks, you can build a highly efficient RAG pipeline right in .NET using the ONNX Runtime. This approach is perfect for desktop applications, edge devices, or keeping your data private.\nThe RAG Workflow # A RAG system has two main stages:\nIndexing (Offline):\nLoad your documents (text files, PDFs, etc.). Chunk the documents into manageable pieces. Use an embedding model to convert each chunk into a numerical vector. Store these vectors in a Vector Database. Querying (Online):\nTake a user\u0026rsquo;s question and convert it into a vector using the same embedding model. Perform a similarity search in the Vector Database to find the most relevant document chunks. Construct a prompt that includes the user\u0026rsquo;s question and the retrieved chunks. Send this augmented prompt to an LLM to generate the final answer. Tools for the Job # .NET 8+ Microsoft.ML.OnnxRuntime: To run our embedding model locally. Microsoft.ML.Tokenizers: The official .NET library for tokenizing text, essential for preparing input for the model. A Sentence-Transformer Model in ONNX format: Models like all-MiniLM-L6-v2 are excellent for this. You can find them on the Hugging Face Hub. A Vector Store: For this example, we\u0026rsquo;ll build a simple in-memory store, but you could use a dedicated library like Microsoft.KernelMemory or a database like ChromaDB. Step 1: The ONNX Embedding Service # First, let\u0026rsquo;s create a service that can take text and turn it into a vector using an ONNX model. You\u0026rsquo;ll need to download the model.onnx file from a sentence-transformer repository on Hugging Face.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using Microsoft.ML.Tokenizers; using System.Numerics.Tensors; // For TensorPrimitives in .NET 8 public class EmbeddingService : IDisposable { private readonly InferenceSession _session; private readonly Tokenizer _tokenizer; public EmbeddingService(string modelPath, string tokenizerPath) { _session = new InferenceSession(modelPath); // Load the tokenizer from the tokenizer.json file (downloaded from Hugging Face) _tokenizer = new Tokenizer(File.OpenRead(tokenizerPath), TokenizerModel.Bert); // Note: Adjust TokenizerModel based on your specific model (e.g., Bert, Bpe) } public float[] GetEmbedding(string text) { // 1. Tokenize the input var tokens = _tokenizer.Encode(text); // 2. Create input tensors // Most BERT-based models expect input_ids, attention_mask, and token_type_ids var inputIds = new DenseTensor\u0026lt;long\u0026gt;(tokens.Ids.Select(x =\u0026gt; (long)x).ToArray(), new[] { 1, tokens.Ids.Count }); var attentionMask = new DenseTensor\u0026lt;long\u0026gt;(Enumerable.Repeat(1L, tokens.Ids.Count).ToArray(), new[] { 1, tokens.Ids.Count }); var tokenTypeIds = new DenseTensor\u0026lt;long\u0026gt;(Enumerable.Repeat(0L, tokens.Ids.Count).ToArray(), new[] { 1, tokens.Ids.Count }); var inputs = new List\u0026lt;NamedOnnxValue\u0026gt; { NamedOnnxValue.CreateFromTensor(\u0026#34;input_ids\u0026#34;, inputIds), NamedOnnxValue.CreateFromTensor(\u0026#34;attention_mask\u0026#34;, attentionMask), NamedOnnxValue.CreateFromTensor(\u0026#34;token_type_ids\u0026#34;, tokenTypeIds) }; // 3. Run inference using var results = _session.Run(inputs); // 4. Extract the embeddings // Usually the last hidden state or a specific pooling layer. // For sentence-transformers, we often take the mean pooling or the CLS token. // This example assumes the model returns a \u0026#39;last_hidden_state\u0026#39; and we take the first token (CLS) for simplicity. // Check your specific model\u0026#39;s output names! var output = results.First().AsTensor\u0026lt;float\u0026gt;(); // Extract the embedding for the first token (CLS token) // Shape is [BatchSize, SequenceLength, HiddenSize] -\u0026gt; [1, SeqLen, 384] var embedding = new float[output.Dimensions[2]]; for (int i = 0; i \u0026lt; embedding.Length; i++) { embedding[i] = output[0, 0, i]; } // 5. Normalize // Calculate the Euclidean norm (L2 norm) of the embedding vector float norm = TensorPrimitives.Norm(embedding); // Divide each element by the norm to get a unit vector TensorPrimitives.Divide(embedding, norm, embedding); return embedding; } public void Dispose() =\u0026gt; _session?.Dispose(); } Note: The Microsoft.ML.Tokenizers library is evolving. Ensure you check the documentation for the specific version you are using. Also, different models have different input requirements (some don\u0026rsquo;t need token_type_ids) and output structures. Always inspect your ONNX model using a tool like Netron to verify input/output names and shapes.\nStep 2: In-Memory Vector Store # Next, a simple class to hold our document chunks and their vectors.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 using System.Numerics.Tensors; // Requires .NET 8+ public class VectorStore { private readonly List\u0026lt;(string Text, float[] Vector)\u0026gt; _vectors = new(); public void Add(string text, float[] vector) { _vectors.Add((text, vector)); } public string FindMostSimilar(float[] queryVector) { if (!_vectors.Any()) return \u0026#34;No documents in store.\u0026#34;; var bestMatch = _vectors .Select(v =\u0026gt; new { Text = v.Text, // High-performance Cosine Similarity using .NET 8 TensorPrimitives Similarity = TensorPrimitives.CosineSimilarity(v.Vector, queryVector) }) .OrderByDescending(x =\u0026gt; x.Similarity) .FirstOrDefault(); return bestMatch?.Text ?? \u0026#34;No similar documents found.\u0026#34;; } } Step 3: Putting It All Together # Now, we can orchestrate the RAG pipeline.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static void Main(string[] args) { // You can download these files from the Hugging Face model page (e.g., sentence-transformers/all-MiniLM-L6-v2) var modelPath = \u0026#34;model.onnx\u0026#34;; var tokenizerPath = \u0026#34;tokenizer.json\u0026#34;; using var embeddingService = new EmbeddingService(modelPath, tokenizerPath); var vectorStore = new VectorStore(); // --- Indexing Stage --- var documents = new[] { \u0026#34;The capital of France is Paris.\u0026#34;, \u0026#34;Photosynthesis is the process used by plants to convert light into energy.\u0026#34;, \u0026#34;The .NET Framework was first released in 2002.\u0026#34; }; Console.WriteLine(\u0026#34;Indexing documents...\u0026#34;); foreach (var doc in documents) { var vector = embeddingService.GetEmbedding(doc); vectorStore.Add(doc, vector); } Console.WriteLine(\u0026#34;Indexing complete.\u0026#34;); // --- Querying Stage --- var userQuestion = \u0026#34;When was .NET released?\u0026#34;; Console.WriteLine($\u0026#34;\\nUser Question: {userQuestion}\u0026#34;); var queryVector = embeddingService.GetEmbedding(userQuestion); var retrievedContext = vectorStore.FindMostSimilar(queryVector); Console.WriteLine($\u0026#34;Retrieved Context: {retrievedContext}\u0026#34;); // --- Augment and Generate Stage --- var prompt = $@\u0026#34; Context: {retrievedContext} Question: {userQuestion} Answer based only on the context provided: \u0026#34;; Console.WriteLine(\u0026#34;\\n--- Augmented Prompt ---\u0026#34;); Console.WriteLine(prompt); // Send this prompt to your chosen LLM (e.g., via OpenAI\u0026#39;s API) // var answer = llmClient.Generate(prompt); // Console.WriteLine($\u0026#34;\\nFinal Answer: {answer}\u0026#34;); } Conclusion # Building a RAG pipeline in C# with ONNX is not only possible but also incredibly powerful. It allows you to create private, efficient, and domain-specific AI applications without relying on external services for the core semantic search functionality. As the .NET AI ecosystem continues to grow, expect the tokenization and vector database steps to become even easier to implement.\nFurther Reading # ONNX Runtime on GitHub - The official ONNX Runtime repository Hugging Face Model Hub - Find pre-trained sentence transformer models Microsoft.KernelMemory - A comprehensive RAG solution for .NET Sentence Transformers Documentation - Learn more about embedding models ","date":"28 September 2025","externalUrl":null,"permalink":"/posts/building-a-rag-system-with-onnx-runtime-and-csharp/","section":"Posts","summary":"","title":"Building a RAG System with ONNX Runtime and C#","type":"posts"},{"content":"","date":"28 September 2025","externalUrl":null,"permalink":"/tags/semantic-search/","section":"Tags","summary":"","title":"Semantic Search","type":"tags"},{"content":" Bio # I am a software developer, data geek, photographer, and gadget aficionado with a passion for building innovative solutions. My interests lie in exploring new technologies and applying them to solve real-world problems. I am a strong advocate for open-source software and enjoy contributing to the community.\nSkills \u0026amp; Technologies # Development \u0026amp; Frameworks # .NET Ecosystem: Modern .NET Stack (8+), C#, ASP.NET Core, Entity Framework Core, WPF, MAUI, WinUI 3 Web Development: Blazor (Server \u0026amp; WASM), PHP, HTML5/CSS3, TypeScript, Tailwind CSS, Accessibility (WCAG) Cloud \u0026amp; DevOps # Cloud Platforms: Microsoft Azure (App Service, Functions, SQL, Cosmos DB, OpenAI) DevOps \u0026amp; CI/CD: Azure DevOps, GitHub Actions, Docker, Kubernetes, Helm, Terraform, Bicep Version Control: Git, GitHub, GitLab Testing \u0026amp; Automation # Test Frameworks: Playwright, FlaUI AI in QA: Developing self-healing test frameworks using LLMs AI \u0026amp; Machine Learning # Generative AI: LLMs (GPT-4, Claude, Llama, Gemini), Image Generation (Stable Diffusion, DALL-E) Agentic AI: Microsoft Agent Framework, Semantic Kernel, Langflow, AutoGen Local AI \u0026amp; Tools: Ollama, LM Studio, Hugging Face, AI CLI Tools Tools \u0026amp; Environment # IDEs \u0026amp; Editors: VS Code, Visual Studio, JetBrains Rider Database Tools: SSMS, Azure Data Studio Operating Systems: Linux (Ubuntu/Debian), macOS, Windows 11/WSL2 Interests \u0026amp; Hobbies # Smart Home: Home Assistant, ESPHome, Zigbee/Z-Wave, Node-RED, MQTT IoT: Custom ESP32/Arduino Projects Profile # Practical AI | Senior .NET Developer | Software Development Team Lead | Consulting Location: Virginia, United States Contact: cmalpass@gmail.com | LinkedIn Profile\nSummary # Results-driven software development leader with over a decade of experience in .NET technologies, specializing in C#, ASP.NET, and WPF. Adept at managing cross-functional teams, delivering innovative solutions, and fostering strong client relationships. Passionate about leveraging technology to solve business challenges and drive efficiency. Proven track record in team mentorship, application development consulting, and project delivery for diverse industries. Looks for creative things to do in all areas of life, personal and professional. Skills \u0026amp; Certifications # Certifications # Exam 487: Developing Microsoft Azure and Web Services Microsoft Certified: Azure Fundamentals Microsoft Certified: Azure AI Engineer Associate AZ-400: Designing and Implementing Microsoft DevOps Solutions MCSD: App Builder - Certified 2017 Experience # Marathon Consulting # (Total Duration: 10 years 3 months)\nApplication Solutions Architect October 2023 - Present (2 years 3 months) | Virginia, United States\nProvide architectural guidance for software development and DevOps CI/CD in support of clients. Work across a variety of client teams providing direction, structure, and mentorship, including supporting a long-time client of over 7 years. Solutions include web applications, web APIs, API clients, WPF desktop/console applications, bash scripts, and Python tools. Primary Tools: .NET, WinForms, WebForms, WPF, Caliburn Micro, REST, OpenAPI, AutoRest, Visual Studio, VS Code, SQL, CSS, HTML5, JavaScript, Angular, Python, C, C#, PowerBI, Bootstrap, Azure DevOps, Docker, Linux, Ubuntu, Virtual Machines, XAML, .NET Framework, .NET Core, .NET 6/7/8, TFS, Azure DevOps Server 2022, MVC, MVVM. Team Lead August 2022 - Present (3 years 5 months) | Virginia Beach, Virginia, United States\nServe as a main point of contact for a group of consultants to guide, mentor, and facilitate professional development aligning with their personal goals. Regularly meet with local and remote team members individually and as a group. Provide feedback on consultant engagements and identify issues or opportunities arising during business. Senior Application Development Consultant April 2018 - November 2023 (5 years 8 months) | Virginia Beach, Virginia\nServed as lead developer for a global heavy manufacturing client, heading a small team to develop tools for mining equipment configuration and troubleshooting. Acted as lead architect and developer on projects ranging from small Angular web apps to full-scale web and desktop solutions for managing global mining fleets. Pushed for the implementation of TFS/Azure DevOps to streamline pipelines for nearly 80 software libraries and applications. Served as subject matter expert for the client regarding Azure DevOps, leading other teams in implementing CI/CD and build/deployment pipelines. Primary Tools: Similar to previous roles, including .NET ecosystem, Angular, Python, Azure DevOps, Docker, and Linux environments. Information Technology Consultant October 2015 - April 2018 (2 years 7 months) | Virginia Beach, Virginia\nInvolved in designing, developing, and implementing web and desktop applications using the .NET ecosystem (Azure, .NET 4.0/4.5, C#, MSSQL) and LAMP stack. Contributed to web application development using Wordpress, Umbraco 7, Vue.js, jQuery, Angular, MQTT, and SignalR. Borrell Associates, Inc # Director of Information Technology February 2009 - October 2015 (6 years 9 months)\nDesigned, built, and maintained scalable web applications with heavy MySQL integration on CentOS hosts (cloud and traditional). Utilized Apache, Nginx, Node.js, Symfony2, CakePHP, and PHP5 for user-friendly apps with optimized database performance. Specialized in data-driven web applications using jQuery, HTML5/CSS3, PHP, MySQL, Wordpress, Joomla!, Highcharts, and SVG maps. Provided expertise in videography, photography, and event coordination. Resort Solutions Inc # Systems Administrator, Analyst, Web Programmer September 2007 - February 2009 (1 year 6 months) | Williamsburg, Virginia\nKey Achievements: Designed and implemented a comprehensive CMS for multiple corporate sales websites. Provided IT support including setting up VPN servers and remote desktop assistance. Managed a single-server Windows 2000 environment and multi-site systems. Developed websites using PHP, MySQL, JavaScript/jQuery, and CSS. Implemented SEO techniques to increase traffic and conversion. Created SQL-based reports for research and sales analysis. Duties: Assumed sole responsibility for IT support for the organization across Mexico and the USA. Maintained PCs, laptops, servers, and IT equipment. Used cPanel and Plesk web server management suites. City of Norfolk # Information Technology Assistant September 2005 - October 2007 (2 years 2 months)\nProvided continuous support for city government information systems and customer support to police, fire, and city hall departments. Installed, configured, and troubleshot hardware including workstations, wireless broadband, and stenography machines. Managed software such as Microsoft Office, GIS systems, and Police Information Systems. Old Dominion University # Web Support Personnel February 2005 - September 2007 (2 years 8 months)\nDeveloped methodology and standards for university websites. Collaborated with support personnel and department liaisons to update websites to current templates. Utilized PHP, XHTML, JavaScript, CSS, and WebEdit for accessible, template-driven websites. Education # Old Dominion University: Bachelor of Science, Business Administration; Information Systems (2003 - 2008) Yonsei University: Exchange Student, International Business, Korean Language, Political Science (2007) ","date":"1 January 2024","externalUrl":null,"permalink":"/about/","section":"About Me","summary":"","title":"About Me","type":"about"},{"content":"I\u0026rsquo;m currently in the process of curating a selection of my favorite projects to showcase here. Over the years, I\u0026rsquo;ve had the privilege of working with a wide variety of clients—from agile startups to global enterprises—on everything from high-scale web applications to the complex, real-time systems powering autonomous vehicles.\nWhether it\u0026rsquo;s architecting distributed systems in .NET, diving deep into AI-driven automation, or building robust desktop experiences, I\u0026rsquo;ve always focused on solving real-world problems with the right tool for the job.\nStay tuned! I\u0026rsquo;ll be adding detailed case studies and deep dives into the architecture of some of these kinds of projects soon. In the meantime, feel free to check out my blog for some of my latest technical explorations.\n","date":"1 January 2024","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":" title: \u0026ldquo;Contact\u0026rdquo; date: 2024-01-01 draft: false description: \u0026ldquo;Get in touch\u0026rdquo; # I\u0026rsquo;m always open to discussing new projects, creative ideas, or to lend a helping hand.\nGet in Touch # Name: Chris Malpass Email: chris@chrismalpass.com Twitter: @cmalpass LinkedIn: Chris Malpass Location # Based in Southest VA (Hampton Roads).\n","externalUrl":null,"permalink":"/contact/","section":"","summary":"","title":"","type":"contact"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]