Noundry.Connector.Cli 1.0.0

dotnet tool install --global Noundry.Connector.Cli --version 1.0.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Noundry.Connector.Cli --version 1.0.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Noundry.Connector.Cli&version=1.0.0
                    
nuke :add-package Noundry.Connector.Cli --version 1.0.0
                    

Noundry.Connector

NuGet Version .NET Version License Build Status

A powerful, flexible API Connector library built on Refit that provides strongly-typed HTTP clients with automatic authentication, comprehensive CRUD operations, and advanced LINQ querying capabilities. Designed for modern .NET applications that demand type safety, performance, and developer productivity.

๐ŸŽฏ Get Started in 30 Seconds

Option 1: Use the CLI Generator (Recommended)

# Install the code generator tool
dotnet tool install -g Noundry.Connector.Generator

# Generate a complete API client
connector-gen generate

Option 2: Manual Setup

# Install the core library
dotnet add package Noundry.Connector

๐Ÿ’ก New! The CLI tool automatically generates strongly-typed clients, models, and configuration for any REST API. See full walkthrough below.

๐Ÿš€ Why Strongly-Typed API Models?

Traditional API consumption often relies on weakly-typed approaches like dynamic, JObject, or raw JSON strings. This library champions strongly-typed models for several critical reasons:

โœ… Compile-Time Safety

// โŒ Weak typing - Runtime errors waiting to happen
dynamic user = await GetUserAsync(1);
string email = user.Email; // What if API returns "email" instead of "Email"?
int posts = user.PostCount; // What if this is a string?

// โœ… Strong typing - Errors caught at compile time
User user = await GetUserAsync(1);
string email = user.Email; // Guaranteed to exist and be correct type
int posts = user.PublicRepos; // Type-safe, IDE autocomplete, refactoring support

๐ŸŽฏ IntelliSense & Developer Experience

// With strongly-typed models, your IDE provides:
// - Autocomplete for all properties
// - Inline documentation
// - Go-to-definition navigation  
// - Refactoring safety across your entire codebase

var userEmail = user.Email;        // โœ… IDE suggests all available properties
var userLocation = user.Address.City; // โœ… Nested navigation works perfectly

๐Ÿ”ง Refactoring & Maintenance

// When the API changes from "email" to "emailAddress":

// โŒ Dynamic approach - Silent runtime failures
dynamic user = await GetUserAsync(1);
string email = user.email; // Breaks silently, returns null

// โœ… Strongly-typed approach - Immediate compile-time feedback
User user = await GetUserAsync(1);
string email = user.Email; // Compiler error forces you to update the model

โšก Performance Benefits

  • No reflection overhead - Direct property access vs. dictionary lookups
  • Optimized serialization - System.Text.Json can pre-compile serializers
  • Memory efficiency - Specific types vs. generic containers
  • Better GC pressure - Value types, immutable strings, optimized allocations

๐ŸŒŸ Features

๐ŸŽฎ CLI Code Generator (New!)

  • ๐Ÿง™โ€โ™‚๏ธ Interactive Wizard: Beautiful Spectre.Console interface guides you through setup
  • ๐Ÿ” Auto-Discovery: Automatically finds API endpoints from OpenAPI/Swagger specs
  • ๐Ÿ“Š Schema Analysis: Analyzes JSON responses to generate strongly-typed models
  • ๐Ÿ—๏ธ Complete Generation: Models, interfaces, clients, and DI extensions
  • ๐Ÿ” Multi-Auth Support: API Key, Bearer Token, Basic Auth, and OAuth 2.0

๐Ÿ›๏ธ Core Library

  • ๐Ÿ” Complete OAuth 2.0: Client Credentials, Authorization Code (PKCE), Password, Device Code flows
  • ๐ŸŽฏ Type-Safe API Clients: Refit-powered strongly-typed HTTP clients
  • ๐Ÿ”„ Full CRUD Operations: Generic interfaces for Create, Read, Update, Delete
  • ๐Ÿ“„ Automatic Pagination: Transparently fetch all pages with IAsyncEnumerable - supports Page, Offset, Cursor, and Link Header styles
  • ๐Ÿ’พ Smart Caching: ETag and Last-Modified support with automatic cache invalidation
  • โšก Rate Limiting: Client-side rate limiting with token bucket algorithm and 429 handling
  • ๐Ÿ”€ Request Deduplication: Share in-flight requests to prevent duplicate API calls
  • ๐Ÿ“Š OpenTelemetry: Built-in metrics and distributed tracing support
  • ๐Ÿ” Advanced LINQ Support: Query API responses like in-memory collections
  • โš™๏ธ Dependency Injection: Seamless integration with Microsoft DI container
  • ๐Ÿ—๏ธ Flexible Architecture: Generic base classes, configurable options
  • ๐Ÿ“ฆ Multi-Target Support: .NET 8.0, .NET 9.0, and .NET 10.0
  • ๐Ÿงช Production Ready: Comprehensive test suite with real API integration

๐Ÿ“‹ Table of Contents

  1. ๐ŸŽฎ CLI Tool Walkthrough
  2. Quick Start
  3. Installation
  4. Authentication
  5. Automatic Pagination
  6. Smart Caching
  7. Rate Limiting
  8. Request Deduplication
  9. OpenTelemetry
  10. Strongly-Typed Models
  11. CRUD Operations
  12. LINQ Querying
  13. Real-World Examples
  14. Best Practices
  15. Performance Benchmarks
  16. Migration Guide

๐ŸŽฎ CLI Tool Walkthrough

The Noundry.Connector.Generator is a wizard-style CLI tool that automatically generates strongly-typed API clients for any REST API. Here's a complete walkthrough:

Installation

# Install globally
dotnet tool install -g Noundry.Connector.Generator

# Or install locally in your project
dotnet new tool-manifest
dotnet tool install Noundry.Connector.Generator

Complete Console Walkthrough

Here's what happens when you run the generator:

$ connector-gen generate

 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— 
โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•‘        โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•
โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•‘        โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•‘   โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘
 โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•  โ•šโ•โ•โ•โ•โ•šโ•โ•  โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•   โ•šโ•โ•    โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•  โ•šโ•โ•
                                                                                   
 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—   
โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—  
โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•  
โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—  
โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘  
 โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•  โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•  โ•šโ•โ•โ•šโ•โ•  โ•šโ•โ•   โ•šโ•โ•    โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•  โ•šโ•โ•  

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿš€ API Client Code Generator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                             โ”‚
โ”‚ Welcome to Noundry Connector Generator!                                    โ”‚
โ”‚                                                                             โ”‚
โ”‚ This wizard will help you:                                                 โ”‚
โ”‚ โ€ข Connect to any REST API                                                  โ”‚
โ”‚ โ€ข Generate strongly-typed C# models                                        โ”‚
โ”‚ โ€ข Create Refit interfaces                                                  โ”‚
โ”‚ โ€ข Build ready-to-use API clients                                           โ”‚
โ”‚                                                                             โ”‚
โ”‚ Powered by Noundry.Connector                                               โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐ŸŒ API Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Enter the API base URL: https://jsonplaceholder.typicode.com
Enter a name for your API client [JsonPlaceholder]: 
Enter the namespace for generated classes [MyApp.ApiClients.JsonPlaceholder]: 

โ ‹ Testing API connectivity...
โœ“ API is accessible

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿ” Authentication Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Select authentication method:
โฏ No Authentication
  API Key  
  Bearer Token
  Basic Authentication (Username/Password)
  OAuth 2.0 (Client ID/Secret)

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿ” Endpoint Discovery โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Automatically discover API endpoints? [y/n] (y): y

โ ™ Discovering API endpoints...
โœ“ Found 20 endpoints

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Method โ”‚ Path              โ”‚ Description                                โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ GET    โ”‚ /posts            โ”‚ GET /posts                                 โ”‚
โ”‚ GET    โ”‚ /posts/{id}       โ”‚ GET /posts/{id}                           โ”‚
โ”‚ POST   โ”‚ /posts            โ”‚ Create new post                           โ”‚
โ”‚ PUT    โ”‚ /posts/{id}       โ”‚ Update post                               โ”‚
โ”‚ DELETE โ”‚ /posts/{id}       โ”‚ Delete post                               โ”‚
โ”‚ GET    โ”‚ /users            โ”‚ GET /users                                โ”‚
โ”‚ GET    โ”‚ /users/{id}       โ”‚ GET /users/{id}                           โ”‚
โ”‚ GET    โ”‚ /comments         โ”‚ GET /comments                             โ”‚
โ”‚ ...    โ”‚ ...               โ”‚ (12 more endpoints)                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Add manual endpoints for testing? [y/n] (n): n

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿ“Š Schema Analysis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

โ ด Analyzing API responses...
โœ“ Analyzed 8 response schemas

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Model Name โ”‚ Properties โ”‚ Type       โ”‚                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                        โ”‚
โ”‚ Post       โ”‚ 4          โ”‚ Object     โ”‚                        โ”‚
โ”‚ User       โ”‚ 8          โ”‚ Object     โ”‚                        โ”‚
โ”‚ Comment    โ”‚ 5          โ”‚ Object     โ”‚                        โ”‚
โ”‚ Address    โ”‚ 7          โ”‚ Object     โ”‚                        โ”‚
โ”‚ Company    โ”‚ 3          โ”‚ Object     โ”‚                        โ”‚
โ”‚ Geo        โ”‚ 2          โ”‚ Object     โ”‚                        โ”‚
โ”‚ ...        โ”‚ ...        โ”‚ (2 more models)                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โš™๏ธ Generation Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Output directory [./Generated]: ./MyApi
Generate model classes? [y/n] (y): y
Generate Refit interface? [y/n] (y): y  
Generate client wrapper class? [y/n] (y): y
Generate DI extension methods? [y/n] (y): y
Use C# records for models? [y/n] (y): y
Use nullable reference types? [y/n] (y): y

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Generation Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                       โ”‚
โ”‚ API Name      โ”‚ JsonPlaceholder                                       โ”‚
โ”‚ Namespace     โ”‚ MyApp.ApiClients.JsonPlaceholder                      โ”‚
โ”‚ Output Path   โ”‚ ./MyApi                                               โ”‚
โ”‚ Generate Models โ”‚ Yes                                                 โ”‚
โ”‚ Generate Interface โ”‚ Yes                                              โ”‚
โ”‚ Generate Client โ”‚ Yes                                                 โ”‚
โ”‚ Use Records   โ”‚ Yes                                                   โ”‚
โ”‚                                                                       โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Proceed with code generation? [y/n] (y): y

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐Ÿ”ง Code Generation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

โ ฆ Generating code...
โ ด Writing files...

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๐ŸŽ‰ Success! โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                     โ”‚
โ”‚ โœ“ Code generation completed successfully!                           โ”‚
โ”‚                                                                     โ”‚
โ”‚ Generated files in: ./MyApi                                         โ”‚
โ”‚                                                                     โ”‚
โ”‚ Next steps:                                                         โ”‚
โ”‚ 1. Add the generated files to your project                          โ”‚
โ”‚ 2. Install Noundry.Connector NuGet package                          โ”‚
โ”‚ 3. Configure DI in your startup code                                โ”‚
โ”‚ 4. Start using your strongly-typed API client!                      โ”‚
โ”‚                                                                     โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Generated File Structure

After running the generator, you'll find:

MyApi/
โ”œโ”€โ”€ Models/
โ”‚   โ”œโ”€โ”€ Post.cs           # public record Post
โ”‚   โ”œโ”€โ”€ User.cs           # public record User  
โ”‚   โ”œโ”€โ”€ Comment.cs        # public record Comment
โ”‚   โ”œโ”€โ”€ Address.cs        # public record Address
โ”‚   โ””โ”€โ”€ Company.cs        # public record Company
โ”œโ”€โ”€ IJsonPlaceholderApi.cs    # Refit interface
โ”œโ”€โ”€ JsonPlaceholderClient.cs  # Client wrapper class  
โ””โ”€โ”€ JsonPlaceholderExtensions.cs  # DI extensions

Example Generated Code

Model (Post.cs):

using System.Text.Json.Serialization;

namespace MyApp.ApiClients.JsonPlaceholder.Models;

/// <summary>
/// Post model from JsonPlaceholder API
/// </summary>
public record Post
{
    [JsonPropertyName("id")]
    public int Id { get; init; }
    
    [JsonPropertyName("userId")]
    public int UserId { get; init; }
    
    [JsonPropertyName("title")]
    public string Title { get; init; }
    
    [JsonPropertyName("body")]
    public string Body { get; init; }
}

Interface (IJsonPlaceholderApi.cs):

using Refit;
using MyApp.ApiClients.JsonPlaceholder.Models;

namespace MyApp.ApiClients.JsonPlaceholder;

public interface IJsonPlaceholderApi
{
    [Get("/posts")]
    Task<List<Post>> GetAllPostsAsync(CancellationToken cancellationToken = default);
    
    [Get("/posts/{id}")]
    Task<Post> GetPostAsync(int id, CancellationToken cancellationToken = default);
    
    [Post("/posts")]
    Task<Post> CreatePostAsync([Body] Post request, CancellationToken cancellationToken = default);
}

Integration in Your App

1. Install Noundry.Connector:

dotnet add package Noundry.Connector

2. Configure DI (Program.cs):

using MyApp.ApiClients.JsonPlaceholder.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add the generated API client
builder.Services.AddJsonPlaceholderClient("https://jsonplaceholder.typicode.com");

var app = builder.Build();

3. Use in Your Services:

public class PostService
{
    private readonly JsonPlaceholderClient _client;
    
    public PostService(JsonPlaceholderClient client)
    {
        _client = client;
    }
    
    public async Task<IEnumerable<Post>> GetRecentPostsAsync()
    {
        var posts = await _client.GetAllPostsAsync();
        
        // Use LINQ to filter and query
        return posts
            .Where(p => p.UserId <= 5)
            .OrderByDescending(p => p.Id)
            .Take(10);
    }
}

Command Line Options

# Interactive mode (default)
connector-gen generate

# Non-interactive mode
connector-gen generate --api-url https://api.example.com --output ./Generated --interactive false

# Get help
connector-gen --help
connector-gen generate --help

๐Ÿš€ Quick Start

Generate a complete API client with a single command:

# 1. Install the generator tool
dotnet tool install -g Noundry.Connector.Generator

# 2. Run the interactive wizard
connector-gen generate

# 3. Follow the prompts - done! ๐ŸŽ‰

The CLI tool will:

  • โœ… Test API connectivity
  • โœ… Discover endpoints automatically
  • โœ… Analyze response schemas
  • โœ… Generate strongly-typed models
  • โœ… Create Refit interfaces
  • โœ… Build client wrapper classes
  • โœ… Set up dependency injection

Option 2: Manual Installation

dotnet add package Noundry.Connector

Basic Setup

using Microsoft.Extensions.DependencyInjection;
using Noundry.Connector.Extensions;
using Noundry.Connector.Authentication;

var services = new ServiceCollection();

// Configure with automatic token authentication
services.AddConnector<IYourApi, YourEntity, int>(options =>
{
    options.BaseUrl = "https://api.yourdomain.com";
    options.DefaultHeaders["User-Agent"] = "MyApp/1.0.0";
}, new TokenAuthenticationProvider("your-api-token"));

var serviceProvider = services.BuildServiceProvider();
var apiClient = serviceProvider.GetRequiredService<IYourApi>();

๐Ÿ“‹ Complete JSONPlaceholder Example

This section provides a comprehensive, step-by-step guide for setting up the JSONPlaceholder API in both console and web applications using the Enterprise API Client.

๐Ÿ“ Step 1: Define Your Models

First, create strongly-typed models that match the JSONPlaceholder API structure:

// Models/User.cs
using System.Text.Json.Serialization;

namespace MyApp.Models;

public class User
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("username")]
    public string Username { get; set; } = string.Empty;

    [JsonPropertyName("email")]
    public string Email { get; set; } = string.Empty;

    [JsonPropertyName("phone")]
    public string Phone { get; set; } = string.Empty;

    [JsonPropertyName("website")]
    public string Website { get; set; } = string.Empty;

    [JsonPropertyName("address")]
    public Address Address { get; set; } = new();

    [JsonPropertyName("company")]
    public Company Company { get; set; } = new();
}

public class Address
{
    [JsonPropertyName("street")]
    public string Street { get; set; } = string.Empty;

    [JsonPropertyName("suite")]
    public string Suite { get; set; } = string.Empty;

    [JsonPropertyName("city")]
    public string City { get; set; } = string.Empty;

    [JsonPropertyName("zipcode")]
    public string Zipcode { get; set; } = string.Empty;

    [JsonPropertyName("geo")]
    public Geo Geo { get; set; } = new();
}

public class Geo
{
    [JsonPropertyName("lat")]
    public string Lat { get; set; } = string.Empty;

    [JsonPropertyName("lng")]
    public string Lng { get; set; } = string.Empty;
}

public class Company
{
    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("catchPhrase")]
    public string CatchPhrase { get; set; } = string.Empty;

    [JsonPropertyName("bs")]
    public string Bs { get; set; } = string.Empty;
}

// Models/Post.cs
public class Post
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("userId")]
    public int UserId { get; set; }

    [JsonPropertyName("title")]
    public string Title { get; set; } = string.Empty;

    [JsonPropertyName("body")]
    public string Body { get; set; } = string.Empty;
}

// Models/Comment.cs
public class Comment
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("postId")]
    public int PostId { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("email")]
    public string Email { get; set; } = string.Empty;

    [JsonPropertyName("body")]
    public string Body { get; set; } = string.Empty;
}

๐Ÿ”Œ Step 2: Define API Interface

Create a Refit interface that defines all available endpoints:

// Services/IJsonPlaceholderApi.cs
using MyApp.Models;
using Refit;

namespace MyApp.Services;

public interface IJsonPlaceholderApi
{
    // Users
    [Get("/users")]
    Task<IEnumerable<User>> GetUsersAsync(CancellationToken cancellationToken = default);

    [Get("/users/{id}")]
    Task<User> GetUserAsync(int id, CancellationToken cancellationToken = default);

    [Post("/users")]
    Task<User> CreateUserAsync([Body] User user, CancellationToken cancellationToken = default);

    [Put("/users/{id}")]
    Task<User> UpdateUserAsync(int id, [Body] User user, CancellationToken cancellationToken = default);

    [Delete("/users/{id}")]
    Task DeleteUserAsync(int id, CancellationToken cancellationToken = default);

    // Posts
    [Get("/posts")]
    Task<IEnumerable<Post>> GetPostsAsync(CancellationToken cancellationToken = default);

    [Get("/posts/{id}")]
    Task<Post> GetPostAsync(int id, CancellationToken cancellationToken = default);

    [Get("/users/{userId}/posts")]
    Task<IEnumerable<Post>> GetPostsByUserAsync(int userId, CancellationToken cancellationToken = default);

    [Post("/posts")]
    Task<Post> CreatePostAsync([Body] Post post, CancellationToken cancellationToken = default);

    [Put("/posts/{id}")]
    Task<Post> UpdatePostAsync(int id, [Body] Post post, CancellationToken cancellationToken = default);

    [Delete("/posts/{id}")]
    Task DeletePostAsync(int id, CancellationToken cancellationToken = default);

    // Comments
    [Get("/comments")]
    Task<IEnumerable<Comment>> GetCommentsAsync(CancellationToken cancellationToken = default);

    [Get("/comments/{id}")]
    Task<Comment> GetCommentAsync(int id, CancellationToken cancellationToken = default);

    [Get("/posts/{postId}/comments")]
    Task<IEnumerable<Comment>> GetCommentsByPostAsync(int postId, CancellationToken cancellationToken = default);

    [Post("/comments")]
    Task<Comment> CreateCommentAsync([Body] Comment comment, CancellationToken cancellationToken = default);

    [Put("/comments/{id}")]
    Task<Comment> UpdateCommentAsync(int id, [Body] Comment comment, CancellationToken cancellationToken = default);

    [Delete("/comments/{id}")]
    Task DeleteCommentAsync(int id, CancellationToken cancellationToken = default);
}

๐Ÿ’ผ Step 3A: Console Application Setup

Create a complete console application with dependency injection:

// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Noundry.Connector.Extensions;
using MyApp.Services;
using MyApp.Models;

// Create host builder for console app with DI
var builder = Host.CreateApplicationBuilder(args);

// Configure logging
builder.Services.AddLogging(configure => configure.AddConsole());

// Configure JSONPlaceholder API client
builder.Services.AddTokenAuthentication("no-auth-required"); // JSONPlaceholder doesn't require auth
builder.Services.AddConnector<IJsonPlaceholderApi>(options =>
{
    options.BaseUrl = "https://jsonplaceholder.typicode.com";
    options.DefaultHeaders["User-Agent"] = "MyConsoleApp/1.0.0";
});

// Register our application service
builder.Services.AddScoped<JsonPlaceholderService>();

var host = builder.Build();

// Run the application
var service = host.Services.GetRequiredService<JsonPlaceholderService>();
await service.RunDemoAsync();

// Services/JsonPlaceholderService.cs
using Microsoft.Extensions.Logging;
using MyApp.Models;

namespace MyApp.Services;

public class JsonPlaceholderService
{
    private readonly IJsonPlaceholderApi _api;
    private readonly ILogger<JsonPlaceholderService> _logger;

    public JsonPlaceholderService(IJsonPlaceholderApi api, ILogger<JsonPlaceholderService> logger)
    {
        _api = api;
        _logger = logger;
    }

    public async Task RunDemoAsync()
    {
        _logger.LogInformation("Starting JSONPlaceholder Demo...");

        try
        {
            // Demo 1: Get all users and display details
            await DemoUsersAsync();

            // Demo 2: Get posts and perform LINQ queries
            await DemoPostsWithLinqAsync();

            // Demo 3: Create, update, delete operations
            await DemoCrudOperationsAsync();

            // Demo 4: Complex cross-entity queries
            await DemoComplexQueriesAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred during demo");
        }

        _logger.LogInformation("Demo completed!");
    }

    private async Task DemoUsersAsync()
    {
        _logger.LogInformation("=== Getting Users ===");

        var users = await _api.GetUsersAsync();
        
        foreach (var user in users.Take(3)) // Show first 3 users
        {
            _logger.LogInformation(
                "User: {Name} ({Email}) from {City}, works at {Company}",
                user.Name, user.Email, user.Address.City, user.Company.Name);
        }

        // Get specific user
        var specificUser = await _api.GetUserAsync(1);
        _logger.LogInformation(
            "User 1 Details: {Name}, Phone: {Phone}, Website: {Website}",
            specificUser.Name, specificUser.Phone, specificUser.Website);
    }

    private async Task DemoPostsWithLinqAsync()
    {
        _logger.LogInformation("=== Posts with LINQ Queries ===");

        var posts = await _api.GetPostsAsync();

        // LINQ Query 1: Posts with long titles
        var longTitlePosts = posts
            .Where(p => p.Title.Length > 50)
            .OrderByDescending(p => p.Title.Length)
            .Take(3)
            .ToList();

        _logger.LogInformation("Posts with long titles (>50 chars):");
        foreach (var post in longTitlePosts)
        {
            _logger.LogInformation("  - {Title} ({Length} chars)", 
                post.Title, post.Title.Length);
        }

        // LINQ Query 2: Posts by specific user
        var userPosts = posts
            .Where(p => p.UserId == 1)
            .ToList();

        _logger.LogInformation("User 1 has {Count} posts", userPosts.Count);

        // LINQ Query 3: Group posts by user
        var postsByUser = posts
            .GroupBy(p => p.UserId)
            .Select(g => new { UserId = g.Key, PostCount = g.Count() })
            .OrderByDescending(x => x.PostCount)
            .Take(3)
            .ToList();

        _logger.LogInformation("Top 3 most active users by post count:");
        foreach (var userStat in postsByUser)
        {
            _logger.LogInformation("  - User {UserId}: {PostCount} posts", 
                userStat.UserId, userStat.PostCount);
        }
    }

    private async Task DemoCrudOperationsAsync()
    {
        _logger.LogInformation("=== CRUD Operations Demo ===");

        // Create a new user
        var newUser = new User
        {
            Name = "John Doe",
            Username = "johndoe",
            Email = "john.doe@example.com",
            Phone = "123-456-7890",
            Website = "johndoe.com",
            Address = new Address
            {
                Street = "123 Main St",
                City = "Anytown",
                Zipcode = "12345"
            },
            Company = new Company
            {
                Name = "Acme Corp",
                CatchPhrase = "Innovation at its best"
            }
        };

        var createdUser = await _api.CreateUserAsync(newUser);
        _logger.LogInformation("Created user with ID: {Id}", createdUser.Id);

        // Update the user
        createdUser.Name = "John Smith";
        createdUser.Email = "john.smith@example.com";
        
        var updatedUser = await _api.UpdateUserAsync(createdUser.Id, createdUser);
        _logger.LogInformation("Updated user name to: {Name}", updatedUser.Name);

        // Create a post for the user
        var newPost = new Post
        {
            UserId = createdUser.Id,
            Title = "My First Blog Post",
            Body = "This is the content of my first blog post. It's quite exciting!"
        };

        var createdPost = await _api.CreatePostAsync(newPost);
        _logger.LogInformation("Created post: '{Title}' (ID: {Id})", 
            createdPost.Title, createdPost.Id);

        // Delete operations (Note: JSONPlaceholder simulates these)
        await _api.DeletePostAsync(createdPost.Id);
        _logger.LogInformation("Deleted post ID: {Id}", createdPost.Id);

        await _api.DeleteUserAsync(createdUser.Id);
        _logger.LogInformation("Deleted user ID: {Id}", createdUser.Id);
    }

    private async Task DemoComplexQueriesAsync()
    {
        _logger.LogInformation("=== Complex Cross-Entity Queries ===");

        // Get all entities
        var users = await _api.GetUsersAsync();
        var posts = await _api.GetPostsAsync();
        var comments = await _api.GetCommentsAsync();

        // Complex Query 1: Users with their post and comment statistics
        var userStats = users
            .Select(u => new
            {
                User = u,
                PostCount = posts.Count(p => p.UserId == u.Id),
                CommentCount = comments.Count(c => 
                    posts.Any(p => p.Id == c.PostId && p.UserId == u.Id))
            })
            .Where(stat => stat.PostCount > 0)
            .OrderByDescending(stat => stat.PostCount + stat.CommentCount)
            .Take(5)
            .ToList();

        _logger.LogInformation("Top 5 most active users (posts + comments on their posts):");
        foreach (var stat in userStats)
        {
            _logger.LogInformation(
                "  - {Name}: {PostCount} posts, {CommentCount} comments on their posts",
                stat.User.Name, stat.PostCount, stat.CommentCount);
        }

        // Complex Query 2: Popular posts (posts with most comments)
        var popularPosts = posts
            .Select(p => new
            {
                Post = p,
                CommentCount = comments.Count(c => c.PostId == p.Id),
                Author = users.First(u => u.Id == p.UserId)
            })
            .Where(p => p.CommentCount > 3)
            .OrderByDescending(p => p.CommentCount)
            .Take(3)
            .ToList();

        _logger.LogInformation("Most popular posts (>3 comments):");
        foreach (var popular in popularPosts)
        {
            _logger.LogInformation(
                "  - '{Title}' by {Author} ({CommentCount} comments)",
                popular.Post.Title.Substring(0, Math.Min(40, popular.Post.Title.Length)),
                popular.Author.Name,
                popular.CommentCount);
        }

        // Complex Query 3: Company analysis
        var companyStats = users
            .GroupBy(u => u.Company.Name)
            .Select(g => new
            {
                Company = g.Key,
                EmployeeCount = g.Count(),
                TotalPosts = g.Sum(u => posts.Count(p => p.UserId == u.Id)),
                Cities = g.Select(u => u.Address.City).Distinct().ToList()
            })
            .Where(c => c.EmployeeCount > 0)
            .OrderByDescending(c => c.TotalPosts)
            .ToList();

        _logger.LogInformation("Company activity analysis:");
        foreach (var company in companyStats)
        {
            _logger.LogInformation(
                "  - {Company}: {Employees} employees, {Posts} total posts, offices in {Cities} cities",
                company.Company,
                company.EmployeeCount,
                company.TotalPosts,
                company.Cities.Count);
        }
    }
}

๐ŸŒ Step 3B: Web Application Setup

Create a complete ASP.NET Core web application:

// Program.cs
using Noundry.Connector.Extensions;
using MyApp.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configure JSONPlaceholder API client
builder.Services.AddTokenAuthentication("no-auth-required");
builder.Services.AddConnector<IJsonPlaceholderApi>(options =>
{
    options.BaseUrl = "https://jsonplaceholder.typicode.com";
    options.DefaultHeaders["User-Agent"] = "MyWebApp/1.0.0";
    options.Timeout = TimeSpan.FromSeconds(30);
});

// Register business services
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IPostService, PostService>();
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

// Controllers/UsersController.cs
using Microsoft.AspNetCore.Mvc;
using MyApp.Services;
using MyApp.Models;

namespace MyApp.Controllers;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;
    private readonly ILogger<UsersController> _logger;

    public UsersController(IUserService userService, ILogger<UsersController> logger)
    {
        _userService = userService;
        _logger = logger;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<User>>> GetUsers()
    {
        try
        {
            var users = await _userService.GetAllUsersAsync();
            return Ok(users);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting users");
            return StatusCode(500, "Internal server error");
        }
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<User>> GetUser(int id)
    {
        try
        {
            var user = await _userService.GetUserByIdAsync(id);
            return Ok(user);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting user {UserId}", id);
            return StatusCode(500, "Internal server error");
        }
    }

    [HttpGet("by-city/{city}")]
    public async Task<ActionResult<IEnumerable<User>>> GetUsersByCity(string city)
    {
        try
        {
            var users = await _userService.GetUsersByCityAsync(city);
            return Ok(users);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting users by city {City}", city);
            return StatusCode(500, "Internal server error");
        }
    }

    [HttpPost]
    public async Task<ActionResult<User>> CreateUser(User user)
    {
        try
        {
            var created = await _userService.CreateUserAsync(user);
            return CreatedAtAction(nameof(GetUser), new { id = created.Id }, created);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating user");
            return StatusCode(500, "Internal server error");
        }
    }
}

// Controllers/AnalyticsController.cs
[ApiController]
[Route("api/[controller]")]
public class AnalyticsController : ControllerBase
{
    private readonly IAnalyticsService _analyticsService;
    private readonly ILogger<AnalyticsController> _logger;

    public AnalyticsController(IAnalyticsService analyticsService, ILogger<AnalyticsController> logger)
    {
        _analyticsService = analyticsService;
        _logger = logger;
    }

    [HttpGet("user-activity")]
    public async Task<ActionResult> GetUserActivity()
    {
        try
        {
            var analytics = await _analyticsService.GetUserActivityAnalyticsAsync();
            return Ok(analytics);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting user activity analytics");
            return StatusCode(500, "Internal server error");
        }
    }

    [HttpGet("popular-posts")]
    public async Task<ActionResult> GetPopularPosts()
    {
        try
        {
            var posts = await _analyticsService.GetPopularPostsAsync();
            return Ok(posts);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting popular posts");
            return StatusCode(500, "Internal server error");
        }
    }
}

// Services/IUserService.cs & UserService.cs
namespace MyApp.Services;

public interface IUserService
{
    Task<IEnumerable<User>> GetAllUsersAsync();
    Task<User> GetUserByIdAsync(int id);
    Task<IEnumerable<User>> GetUsersByCityAsync(string city);
    Task<User> CreateUserAsync(User user);
    Task<User> UpdateUserAsync(int id, User user);
    Task DeleteUserAsync(int id);
}

public class UserService : IUserService
{
    private readonly IJsonPlaceholderApi _api;
    private readonly ILogger<UserService> _logger;

    public UserService(IJsonPlaceholderApi api, ILogger<UserService> logger)
    {
        _api = api;
        _logger = logger;
    }

    public async Task<IEnumerable<User>> GetAllUsersAsync()
    {
        return await _api.GetUsersAsync();
    }

    public async Task<User> GetUserByIdAsync(int id)
    {
        return await _api.GetUserAsync(id);
    }

    public async Task<IEnumerable<User>> GetUsersByCityAsync(string city)
    {
        var users = await _api.GetUsersAsync();
        return users.Where(u => u.Address.City.Contains(city, StringComparison.OrdinalIgnoreCase));
    }

    public async Task<User> CreateUserAsync(User user)
    {
        return await _api.CreateUserAsync(user);
    }

    public async Task<User> UpdateUserAsync(int id, User user)
    {
        return await _api.UpdateUserAsync(id, user);
    }

    public async Task DeleteUserAsync(int id)
    {
        await _api.DeleteUserAsync(id);
    }
}

// Services/IAnalyticsService.cs & AnalyticsService.cs
public interface IAnalyticsService
{
    Task<object> GetUserActivityAnalyticsAsync();
    Task<IEnumerable<object>> GetPopularPostsAsync();
}

public class AnalyticsService : IAnalyticsService
{
    private readonly IJsonPlaceholderApi _api;
    private readonly ILogger<AnalyticsService> _logger;

    public AnalyticsService(IJsonPlaceholderApi api, ILogger<AnalyticsService> logger)
    {
        _api = api;
        _logger = logger;
    }

    public async Task<object> GetUserActivityAnalyticsAsync()
    {
        var users = await _api.GetUsersAsync();
        var posts = await _api.GetPostsAsync();

        var analytics = users
            .Select(u => new
            {
                UserId = u.Id,
                Name = u.Name,
                Email = u.Email,
                Company = u.Company.Name,
                City = u.Address.City,
                PostCount = posts.Count(p => p.UserId == u.Id),
                AvgPostTitleLength = posts
                    .Where(p => p.UserId == u.Id)
                    .Select(p => p.Title.Length)
                    .DefaultIfEmpty(0)
                    .Average()
            })
            .OrderByDescending(a => a.PostCount)
            .ToList();

        return new
        {
            TotalUsers = users.Count(),
            TotalPosts = posts.Count(),
            AveragePostsPerUser = posts.Count() / (double)users.Count(),
            MostActiveUsers = analytics.Take(5),
            UsersByCity = analytics
                .GroupBy(a => a.City)
                .Select(g => new { City = g.Key, UserCount = g.Count() })
                .OrderByDescending(x => x.UserCount),
            CompaniesByActivity = analytics
                .GroupBy(a => a.Company)
                .Select(g => new 
                { 
                    Company = g.Key, 
                    Employees = g.Count(), 
                    TotalPosts = g.Sum(x => x.PostCount) 
                })
                .OrderByDescending(x => x.TotalPosts)
        };
    }

    public async Task<IEnumerable<object>> GetPopularPostsAsync()
    {
        var posts = await _api.GetPostsAsync();
        var users = await _api.GetUsersAsync();
        var comments = await _api.GetCommentsAsync();

        return posts
            .Select(p => new
            {
                Id = p.Id,
                Title = p.Title,
                Body = p.Body.Length > 100 ? p.Body.Substring(0, 100) + "..." : p.Body,
                Author = users.FirstOrDefault(u => u.Id == p.UserId)?.Name ?? "Unknown",
                AuthorCompany = users.FirstOrDefault(u => u.Id == p.UserId)?.Company?.Name ?? "Unknown",
                CommentCount = comments.Count(c => c.PostId == p.Id),
                TitleWordCount = p.Title.Split(' ').Length,
                BodyWordCount = p.Body.Split(' ').Length
            })
            .OrderByDescending(p => p.CommentCount)
            .Take(10);
    }
}

๐Ÿ“ฆ Step 4: Package References

Make sure your project has the required dependencies:


<Project Sdk="Microsoft.NET.Sdk.Web"> 
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework> 
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Noundry.Connector" Version="1.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
    
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>
</Project>

๐Ÿš€ Running the Applications

Console App:

dotnet run

Web App:

dotnet run
# Navigate to https://localhost:5001/swagger to see the API documentation
# Test endpoints:
# GET /api/users
# GET /api/users/1
# GET /api/users/by-city/paris
# GET /api/analytics/user-activity
# GET /api/analytics/popular-posts

๐ŸŽฏ Key Benefits Demonstrated

This complete example showcases:

โœ… Strongly-typed models with full IntelliSense support โœ… Automatic JSON serialization/deserialization with proper attribute mapping โœ… Comprehensive CRUD operations for all entity types โœ… Advanced LINQ queries including cross-entity joins and aggregations
โœ… Dependency injection integration for both console and web applications โœ… Proper error handling and logging throughout the application โœ… Separation of concerns with service layers and controllers โœ… Real-world business logic with analytics and reporting features

๐Ÿ” Authentication

Token Authentication

services.AddTokenAuthentication("your-api-token");

services.AddConnector<IYourApi, Entity, int>(options =>
{
    options.BaseUrl = "https://api.example.com";
}, serviceProvider.GetRequiredService<IAuthenticationProvider>());

OAuth 2.0 Authentication

Basic OAuth 2.0 Setup
services.AddOAuthAuthentication(config =>
{
    config.ClientId = "your-client-id";
    config.ClientSecret = "your-client-secret";
    config.TokenEndpoint = "https://auth.example.com/oauth/token";
    config.Scope = "read write";
});
Complete OAuth 2.0 Example with GitHub API

Here's a comprehensive example showing how to use OAuth 2.0 authentication with the GitHub API:

// Models/GitHubModels.cs
using System.Text.Json.Serialization;

namespace MyApp.Models;

public class GitHubUser
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("login")]
    public string Login { get; set; } = string.Empty;

    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("email")]
    public string? Email { get; set; }

    [JsonPropertyName("avatar_url")]
    public string AvatarUrl { get; set; } = string.Empty;

    [JsonPropertyName("public_repos")]
    public int PublicRepos { get; set; }

    [JsonPropertyName("followers")]
    public int Followers { get; set; }

    [JsonPropertyName("following")]
    public int Following { get; set; }
}

public class GitHubRepository
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("full_name")]
    public string FullName { get; set; } = string.Empty;

    [JsonPropertyName("description")]
    public string? Description { get; set; }

    [JsonPropertyName("private")]
    public bool IsPrivate { get; set; }

    [JsonPropertyName("stargazers_count")]
    public int Stars { get; set; }

    [JsonPropertyName("language")]
    public string? Language { get; set; }

    [JsonPropertyName("created_at")]
    public DateTime CreatedAt { get; set; }
}

// Services/IGitHubApi.cs
using Refit;
using MyApp.Models;

namespace MyApp.Services;

[Headers("User-Agent: MyApp/1.0.0")]
public interface IGitHubApi
{
    [Get("/user")]
    Task<GitHubUser> GetCurrentUserAsync(CancellationToken cancellationToken = default);

    [Get("/user/repos")]
    Task<IEnumerable<GitHubRepository>> GetUserRepositoriesAsync([Query] string? type = null, CancellationToken cancellationToken = default);

    [Get("/repos/{owner}/{repo}")]
    Task<GitHubRepository> GetRepositoryAsync(string owner, string repo, CancellationToken cancellationToken = default);

    [Post("/user/repos")]
    Task<GitHubRepository> CreateRepositoryAsync([Body] CreateRepositoryRequest request, CancellationToken cancellationToken = default);
}

public class CreateRepositoryRequest
{
    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("description")]
    public string? Description { get; set; }

    [JsonPropertyName("private")]
    public bool IsPrivate { get; set; } = false;

    [JsonPropertyName("auto_init")]
    public bool AutoInit { get; set; } = true;
}

// Program.cs (Console Application)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Noundry.Connector.Extensions;
using MyApp.Services;

var builder = Host.CreateApplicationBuilder(args);

// Configure OAuth 2.0 for GitHub
builder.Services.AddOAuthAuthentication(config =>
{
    // These should come from configuration/environment variables
    config.ClientId = builder.Configuration["GitHub:ClientId"] ?? throw new InvalidOperationException("GitHub ClientId not configured");
    config.ClientSecret = builder.Configuration["GitHub:ClientSecret"] ?? throw new InvalidOperationException("GitHub ClientSecret not configured");
    config.TokenEndpoint = "https://github.com/login/oauth/access_token";
    config.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
    config.Scope = "repo user:email";
    config.GrantType = "authorization_code"; // For web apps with redirect
});

// Configure GitHub API client
builder.Services.AddConnector<IGitHubApi>(options =>
{
    options.BaseUrl = "https://api.github.com";
    options.DefaultHeaders["Accept"] = "application/vnd.github.v3+json";
    options.DefaultHeaders["User-Agent"] = "MyGitHubApp/1.0.0";
});

// Register business services
builder.Services.AddScoped<GitHubService>();

var host = builder.Build();

// Run the application
var gitHubService = host.Services.GetRequiredService<GitHubService>();
await gitHubService.RunGitHubDemoAsync();

// Services/GitHubService.cs
using Microsoft.Extensions.Logging;
using MyApp.Models;

namespace MyApp.Services;

public class GitHubService
{
    private readonly IGitHubApi _gitHubApi;
    private readonly ILogger<GitHubService> _logger;

    public GitHubService(IGitHubApi gitHubApi, ILogger<GitHubService> logger)
    {
        _gitHubApi = gitHubApi;
        _logger = logger;
    }

    public async Task RunGitHubDemoAsync()
    {
        try
        {
            _logger.LogInformation("Starting GitHub API Demo with OAuth 2.0...");

            // Get current user info
            var currentUser = await _gitHubApi.GetCurrentUserAsync();
            _logger.LogInformation("Authenticated as: {Login} ({Name})", currentUser.Login, currentUser.Name);
            _logger.LogInformation("Public repos: {Repos}, Followers: {Followers}", 
                currentUser.PublicRepos, currentUser.Followers);

            // Get user's repositories
            var repositories = await _gitHubApi.GetUserRepositoriesAsync("owner");
            _logger.LogInformation("Found {Count} repositories", repositories.Count());

            // Analyze repositories with LINQ
            var repoAnalysis = repositories
                .GroupBy(r => r.Language ?? "Unknown")
                .Select(g => new {
                    Language = g.Key,
                    Count = g.Count(),
                    TotalStars = g.Sum(r => r.Stars),
                    AverageStars = g.Average(r => r.Stars)
                })
                .OrderByDescending(x => x.TotalStars)
                .Take(5)
                .ToList();

            _logger.LogInformation("Top 5 languages by stars:");
            foreach (var lang in repoAnalysis)
            {
                _logger.LogInformation("  {Language}: {Count} repos, {TotalStars} total stars ({AverageStars:F1} avg)",
                    lang.Language, lang.Count, lang.TotalStars, lang.AverageStars);
            }

            // Find most popular repositories
            var popularRepos = repositories
                .Where(r => r.Stars > 0)
                .OrderByDescending(r => r.Stars)
                .Take(3)
                .ToList();

            if (popularRepos.Any())
            {
                _logger.LogInformation("Most popular repositories:");
                foreach (var repo in popularRepos)
                {
                    _logger.LogInformation("  โญ {Name}: {Stars} stars ({Language})",
                        repo.Name, repo.Stars, repo.Language ?? "No language");
                }
            }

            // Example: Create a new repository (commented out to avoid creating test repos)
            /*
            var newRepoRequest = new CreateRepositoryRequest
            {
                Name = "oauth-test-repo",
                Description = "Test repository created via OAuth 2.0 API",
                IsPrivate = false,
                AutoInit = true
            };

            var newRepo = await _gitHubApi.CreateRepositoryAsync(newRepoRequest);
            _logger.LogInformation("Created new repository: {FullName}", newRepo.FullName);
            */

        }
        catch (Refit.ApiException ex)
        {
            _logger.LogError("GitHub API error: {StatusCode} - {Content}", ex.StatusCode, ex.Content);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred during GitHub API demo");
        }
    }
}

// appsettings.json
{
  "GitHub": {
    "ClientId": "your-github-oauth-app-client-id",
    "ClientSecret": "your-github-oauth-app-client-secret"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
Web Application OAuth 2.0 Flow

For web applications, you'll typically need to handle the OAuth authorization flow:

// Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Noundry.Connector.Authentication;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IOAuthAuthenticationProvider _oauthProvider;
    private readonly IConfiguration _configuration;

    public AuthController(IOAuthAuthenticationProvider oauthProvider, IConfiguration configuration)
    {
        _oauthProvider = oauthProvider;
        _configuration = configuration;
    }

    [HttpGet("login")]
    public IActionResult Login()
    {
        var clientId = _configuration["GitHub:ClientId"];
        var redirectUri = _configuration["GitHub:RedirectUri"];
        var scope = "repo user:email";
        var state = Guid.NewGuid().ToString(); // Store this in session/cache for validation

        var authUrl = $"https://github.com/login/oauth/authorize" +
                     $"?client_id={clientId}" +
                     $"&redirect_uri={Uri.EscapeDataString(redirectUri)}" +
                     $"&scope={Uri.EscapeDataString(scope)}" +
                     $"&state={state}";

        return Redirect(authUrl);
    }

    [HttpGet("callback")]
    public async Task<IActionResult> Callback([FromQuery] string code, [FromQuery] string state)
    {
        if (string.IsNullOrEmpty(code))
        {
            return BadRequest("Authorization code not provided");
        }

        // Validate state parameter (implement proper state validation)
        
        try
        {
            // Exchange code for access token
            await _oauthProvider.ExchangeCodeForTokenAsync(code);
            
            return Ok(new { message = "Authentication successful" });
        }
        catch (Exception ex)
        {
            return BadRequest($"Authentication failed: {ex.Message}");
        }
    }
}

// Program.cs (Web Application)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Configure OAuth 2.0
builder.Services.AddOAuthAuthentication(config =>
{
    config.ClientId = builder.Configuration["GitHub:ClientId"]!;
    config.ClientSecret = builder.Configuration["GitHub:ClientSecret"]!;
    config.TokenEndpoint = "https://github.com/login/oauth/access_token";
    config.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
    config.RedirectUri = builder.Configuration["GitHub:RedirectUri"]!;
    config.Scope = "repo user:email";
});

// Configure GitHub API client
builder.Services.AddConnector<IGitHubApi>(options =>
{
    options.BaseUrl = "https://api.github.com";
    options.DefaultHeaders["Accept"] = "application/vnd.github.v3+json";
});

var app = builder.Build();

app.MapControllers();
app.Run();
OAuth 2.0 Configuration Options
services.AddOAuthAuthentication(config =>
{
    config.ClientId = "your-client-id";
    config.ClientSecret = "your-client-secret";
    config.TokenEndpoint = "https://auth.provider.com/oauth/token";
    config.AuthorizationEndpoint = "https://auth.provider.com/oauth/authorize";
    config.RedirectUri = "https://yourapp.com/auth/callback";
    config.Scope = "read write profile";
    config.GrantType = "authorization_code"; // or "client_credentials"
    
    // Token refresh settings
    config.AutoRefreshToken = true;
    config.RefreshTokenBuffer = TimeSpan.FromMinutes(5); // Refresh 5 min before expiry
    
    // Custom headers
    config.TokenRequestHeaders = new Dictionary<string, string>
    {
        ["Accept"] = "application/json",
        ["Content-Type"] = "application/x-www-form-urlencoded"
    };
    
    // PKCE support for security
    config.UsePkce = true;
});
Key Features of OAuth 2.0 Integration

โœ… Automatic Token Management: Handles token acquisition, storage, and refresh โœ… Multiple Grant Types: Client Credentials, Authorization Code, Password, Device Code โœ… PKCE Support: Enhanced security for public clients โœ… Device Code Flow: For CLI tools and devices with limited input โœ… Configurable Scopes: Fine-grained permission control โœ… Token Refresh: Automatic token renewal before expiration โœ… State Validation: CSRF protection for authorization flows โœ… Error Handling: Comprehensive OAuth error handling with typed exceptions

Device Code Flow (CLI/Limited Input Devices)
// Perfect for CLI tools or devices without browsers
var config = OAuthConfiguration.ForDeviceCode(
    clientId: "your-client-id",
    deviceAuthorizationEndpoint: "https://auth.provider.com/device/code",
    tokenEndpoint: "https://auth.provider.com/oauth/token",
    scope: "openid profile");

var httpClient = new HttpClient();
var oauthProvider = new OAuthAuthenticationProvider(httpClient, config);

// Start device authorization
var deviceAuth = await oauthProvider.StartDeviceAuthorizationAsync();

Console.WriteLine($"Please visit: {deviceAuth.VerificationUri}");
Console.WriteLine($"And enter code: {deviceAuth.UserCode}");

// Poll for token (user authorizes in browser)
while (true)
{
    try
    {
        var token = await oauthProvider.PollForDeviceTokenAsync(deviceAuth.DeviceCode);
        Console.WriteLine("Authorization successful!");
        break;
    }
    catch (OAuthPendingException)
    {
        await Task.Delay(deviceAuth.Interval * 1000);
    }
    catch (OAuthSlowDownException)
    {
        await Task.Delay((deviceAuth.Interval + 5) * 1000);
    }
}
Password Grant (Trusted Applications)
// For trusted first-party applications only
var config = OAuthConfiguration.ForPassword(
    clientId: "your-client-id",
    tokenEndpoint: "https://auth.provider.com/oauth/token",
    scope: "read write");

var oauthProvider = new OAuthAuthenticationProvider(httpClient, config);

// Authenticate with username/password
var token = await oauthProvider.AuthenticateWithPasswordAsync("user@example.com", "password");

Custom Authentication

public class CustomAuthProvider : IAuthenticationProvider
{
    public async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
    {
        // Your custom token logic here
        return await GetTokenFromCustomSource();
    }

    public async Task RefreshTokenAsync(CancellationToken cancellationToken = default)
    {
        // Your custom refresh logic here
        await RefreshCustomToken();
    }
}

๐Ÿ“„ Automatic Pagination

One of Noundry.Connector's most powerful features is automatic pagination - transparently fetch all pages from paginated APIs using IAsyncEnumerable. No more manual page loops!

Basic Usage

using Noundry.Connector.Pagination;

// Create a paginated client
var httpClient = new HttpClient { BaseAddress = new Uri("https://api.example.com") };
var client = new PaginatedClient(httpClient);

// Fetch ALL items transparently - pages are fetched on-demand
await foreach (var user in client.GetAllAsync<User>("/users"))
{
    Console.WriteLine(user.Name);
    // Items stream in as pages are fetched
}

// Or collect to a list
var allUsers = await client.GetAllAsync<User>("/users").ToListAsync();

Supported Pagination Styles

Style Description Example
PageBased Traditional page numbers ?page=2&per_page=20
OffsetBased SQL-style offset/limit ?offset=20&limit=20
CursorBased Token-based (Twitter, Slack) ?cursor=abc123&limit=20
LinkHeader GitHub-style Link headers Link: <url>; rel="next"

Pagination Styles

// GitHub-style Link header pagination
var repos = await client
    .GetAllWithLinkHeaderAsync<Repo>("/users/microsoft/repos")
    .ToListAsync();

// Cursor-based pagination (Slack, Twitter)
var messages = await client
    .GetAllWithCursorAsync<Message>("/messages",
        cursorParam: "cursor",
        nextCursorPath: "$.response_metadata.next_cursor")
    .ToListAsync();

// Offset-based pagination
var products = await client
    .GetAllWithOffsetAsync<Product>("/products", pageSize: 50)
    .ToListAsync();

// APIs that return next URL in response body
var results = await client
    .GetAllWithNextUrlAsync<Item>("/search",
        nextUrlPath: "$.next",
        itemsPath: "$.results")
    .ToListAsync();

Advanced Configuration

var options = new PaginationOptions
{
    Style = PaginationStyle.PageBased,
    PageParameter = "p",           // Custom param name
    PageSizeParameter = "size",    // Custom param name
    DefaultPageSize = 50,
    MaxPages = 10,                 // Safety limit
    MaxItems = 500,                // Stop after N items
    ItemsPath = "$.data.items",    // For nested responses
    TotalCountPath = "$.meta.total",
    HasMorePath = "$.meta.has_next",
    DelayBetweenRequests = TimeSpan.FromMilliseconds(100), // Rate limiting
    StopOnEmptyPage = true
};

var items = await client.GetAllAsync<Item>("/items", options).ToListAsync();

IAsyncEnumerable Extensions

Chain operations that work across pages:

// Filter, transform, and limit - stops fetching early when possible!
var activeAdmins = await client
    .GetAllAsync<User>("/users", options)
    .WhereAsync(u => u.IsActive && u.Role == "Admin")
    .SelectAsync(u => new { u.Id, u.Email })
    .TakeAsync(10)  // Stops fetching once we have 10!
    .ToListAsync();

// Find first match - stops immediately when found
var targetUser = await client
    .GetAllAsync<User>("/users", options)
    .FirstOrDefaultAsync(u => u.Email == "admin@example.com");

// Process in batches
await foreach (var batch in client.GetAllAsync<Order>("/orders", options).BatchAsync(100))
{
    await ProcessBatchAsync(batch);
}

// Count with predicate
var activeCount = await client
    .GetAllAsync<User>("/users", options)
    .WhereAsync(u => u.IsActive)
    .CountAsync();

// Check existence efficiently
var hasAdmins = await client
    .GetAllAsync<User>("/users", options)
    .AnyAsync(u => u.Role == "Admin");

Available Extension Methods

Method Description
ToListAsync() Collect all items to a List
TakeAsync(n) Take first N items (stops fetching early!)
SkipAsync(n) Skip first N items
WhereAsync(predicate) Filter items
SelectAsync(selector) Project/transform items
FirstOrDefaultAsync() Get first item or default
CountAsync() Count all items
AnyAsync() Check if any items exist
BatchAsync(size) Group into batches
ForEachAsync(action) Execute action per item
GroupByAsync(keySelector) Group items by key

Early Termination

The pagination engine is smart about stopping early:

// Only fetches 1 page if 5 items fit on first page
var firstFive = await client
    .GetAllAsync<User>("/users")
    .TakeAsync(5)
    .ToListAsync();

// Stops immediately when match found - doesn't fetch remaining pages
var found = await client
    .GetAllAsync<User>("/users")
    .FirstOrDefaultAsync(u => u.Id == 42);

Factory Methods for Common Patterns

// Quick setup with factory methods
var pageOptions = PaginationOptions.PageBased(
    pageParam: "page",
    pageSizeParam: "per_page",
    defaultPageSize: 20,
    startPage: 1);

var offsetOptions = PaginationOptions.OffsetBased(
    offsetParam: "offset",
    limitParam: "limit",
    defaultLimit: 50);

var cursorOptions = PaginationOptions.CursorBased(
    cursorParam: "cursor",
    limitParam: "limit",
    nextCursorPath: "$.next_cursor",
    defaultLimit: 100);

var linkOptions = PaginationOptions.LinkHeader(
    pageSizeParam: "per_page",
    defaultPageSize: 30);

Dependency Injection

// Register pagination services
services.AddHttpClient();
services.AddPagination(options =>
{
    options.DefaultPageSize = 50;
    options.MaxPages = 100;
});

// Inject and use
public class UserService
{
    private readonly IPaginatedClientFactory _clientFactory;

    public UserService(IPaginatedClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task<List<User>> GetAllUsersAsync()
    {
        using var client = _clientFactory.Create("users-api");
        return await client.GetAllAsync<User>("/users").ToListAsync();
    }
}

๐Ÿ’พ Smart Caching + ETags

Noundry.Connector provides intelligent HTTP response caching with full support for ETags, Last-Modified headers, and Cache-Control directives.

Basic Caching Setup

services.AddHttpClient<IMyApi>()
    .AddCaching(options =>
    {
        options.UseETag = true;              // Enable If-None-Match
        options.UseLastModified = true;      // Enable If-Modified-Since
        options.DefaultExpiration = TimeSpan.FromMinutes(5);
    });

How It Works

// First request: Full response (200 OK), cached with ETag
var user = await api.GetUserAsync(1);  // X-Cache: MISS

// Second request within cache window: Returns from cache instantly
var user = await api.GetUserAsync(1);  // X-Cache: HIT (no network)

// After cache expires: Conditional request with If-None-Match
var user = await api.GetUserAsync(1);  // Server returns 304 Not Modified
                                        // X-Cache: HIT (conditional)

Advanced Configuration

services.AddHttpClient<IMyApi>()
    .AddCaching(options =>
    {
        // Cache behavior
        options.UseETag = true;
        options.UseLastModified = true;
        options.DefaultExpiration = TimeSpan.FromMinutes(5);
        options.MaxExpiration = TimeSpan.FromHours(1);

        // What to cache
        options.CacheableMethods = new() { HttpMethod.Get, HttpMethod.Head };

        // What invalidates cache
        options.InvalidatingMethods = new() {
            HttpMethod.Post, HttpMethod.Put,
            HttpMethod.Patch, HttpMethod.Delete
        };

        // Respect server directives
        options.RespectNoStore = true;
        options.RespectNoCache = true;

        // URL filtering
        options.ExcludePatterns = new() { "/health", "/metrics" };
        options.IncludePatterns = new() { "/api/.*" };

        // Cache size
        options.MaxCacheSize = 1000;

        // Custom cache key
        options.CacheKeyGenerator = req => $"{req.Method}:{req.RequestUri}";

        // Callbacks
        options.OnCacheHit = (key, type) =>
            logger.LogDebug("Cache {Type}: {Key}", type, key);
        options.OnCacheMiss = key =>
            logger.LogDebug("Cache MISS: {Key}", key);
    });

Cache Hit Types

Type Description
Full Returned from cache, no network request
Conditional Server returned 304 Not Modified
Stale Returned stale data while revalidating

Custom Cache Store

// Implement your own cache (Redis, SQL, etc.)
public class RedisCacheStore : ICacheStore
{
    public Task<CacheEntry?> GetAsync(string key, CancellationToken ct);
    public Task SetAsync(string key, CacheEntry entry, CancellationToken ct);
    public Task RemoveAsync(string key, CancellationToken ct);
    public Task RemoveByPatternAsync(string pattern, CancellationToken ct);
    public Task ClearAsync(CancellationToken ct);
}

// Use it
services.AddHttpClient<IMyApi>()
    .AddCaching(new RedisCacheStore(redis), options => { ... });

โšก Rate Limiting

Built-in client-side rate limiting protects you from hitting API limits and handles 429 responses gracefully.

Basic Rate Limiting

services.AddHttpClient<IMyApi>()
    .AddRateLimiting(options =>
    {
        options.MaxRequestsPerSecond = 10;
        options.MaxConcurrentRequests = 5;
    });

How It Works

// Requests are automatically throttled
var tasks = Enumerable.Range(1, 100)
    .Select(i => api.GetUserAsync(i));

// Only 10 requests/second, max 5 concurrent
// Excess requests are queued automatically
var results = await Task.WhenAll(tasks);

Advanced Configuration

services.AddHttpClient<IMyApi>()
    .AddRateLimiting(options =>
    {
        // Rate limits
        options.MaxRequestsPerSecond = 10;
        options.MaxConcurrentRequests = 5;

        // Queue behavior
        options.MaxQueueSize = 100;          // Max queued requests
        options.QueueTimeout = TimeSpan.FromSeconds(30);

        // 429 handling
        options.RespectRetryAfterHeader = true;
        options.DefaultRetryAfterDelay = TimeSpan.FromSeconds(1);
        options.MaxRetryAfterDelay = TimeSpan.FromSeconds(60);
        options.MaxRetryOn429 = 3;

        // Algorithm
        options.UseSlidingWindow = true;     // vs fixed window
        options.WindowSize = TimeSpan.FromSeconds(1);

        // Bypass certain URLs
        options.BypassPatterns = new() { "/health", "/status" };

        // Per-endpoint rate limits
        options.RateLimitKeyGenerator = req =>
            req.RequestUri?.AbsolutePath ?? "default";

        // Callbacks
        options.OnRateLimited = evt =>
            logger.LogWarning("Rate limited: waiting {Delay}ms",
                evt.Delay.TotalMilliseconds);
        options.OnRequestRejected = req =>
            logger.LogError("Request rejected: queue full");
    });

Rate Limit Events

options.OnRateLimited = evt =>
{
    Console.WriteLine($"Request: {evt.Request.RequestUri}");
    Console.WriteLine($"Delay: {evt.Delay.TotalMilliseconds}ms");
    Console.WriteLine($"Queue Position: {evt.QueuePosition}");
    Console.WriteLine($"Reason: {evt.Reason}");
    // Reasons: RequestsPerSecond, ConcurrentRequests, ServerRateLimited
};

๐Ÿ”€ Request Deduplication

Automatically share in-flight requests - if the same request is already being made, subsequent callers wait for the existing request instead of making duplicates.

Basic Setup

services.AddHttpClient<IMyApi>()
    .AddDeduplication();

How It Works

// These three calls result in only ONE HTTP request!
var task1 = api.GetUserAsync(1);
var task2 = api.GetUserAsync(1);  // Joins task1
var task3 = api.GetUserAsync(1);  // Joins task1

var results = await Task.WhenAll(task1, task2, task3);
// All three get the same result from a single HTTP call

Configuration

services.AddHttpClient<IMyApi>()
    .AddDeduplication(options =>
    {
        // Only deduplicate safe methods
        options.DeduplicateMethods = new() { HttpMethod.Get, HttpMethod.Head };

        // Max time to wait for in-flight request
        options.MaxWaitTime = TimeSpan.FromSeconds(30);

        // Exclude certain URLs
        options.ExcludePatterns = new() { "/realtime/.*" };

        // Include headers in deduplication key
        options.IncludeHeadersInKey = true;
        options.HeadersForKey = new() { "Authorization", "Accept" };

        // Custom key generator
        options.KeyGenerator = req => $"{req.Method}:{req.RequestUri}";

        // Callback when deduplicated
        options.OnDeduplicated = (key, count) =>
            logger.LogDebug("Deduplicated {Count} requests for {Key}", count, key);
    });

Use Cases

  • UI Components: Multiple components requesting same data simultaneously
  • Parallel Processing: Batch operations that may request same resources
  • Cache Warming: Prevent thundering herd on cache miss
  • API Efficiency: Reduce unnecessary load on backend services

๐Ÿ“Š OpenTelemetry Integration

Built-in observability with OpenTelemetry-compatible metrics and distributed tracing.

Basic Setup

services.AddHttpClient<IMyApi>()
    .AddTelemetry(options =>
    {
        options.ServiceName = "my-api-client";
    });

Metrics Exposed

Metric Type Description
http.client.request.duration Histogram Request duration in milliseconds
http.client.response.size Histogram Response size in bytes
http.client.request.count Counter Request count by status code
http.client.active_requests UpDownCounter Current in-flight requests

Configuration

services.AddHttpClient<IMyApi>()
    .AddTelemetry(options =>
    {
        options.ServiceName = "order-service";

        // Metrics
        options.RecordRequestDuration = true;
        options.RecordResponseSize = true;
        options.RecordStatusCodes = true;
        options.RecordActiveRequests = true;

        // Tracing
        options.EnableTracing = true;
        options.PropagateTraceContext = true;  // Distributed tracing

        // What to include in traces
        options.IncludeHeaders = false;  // May contain sensitive data
        options.ExcludedHeaders = new() { "Authorization", "Cookie" };
        options.IncludeRequestBody = false;
        options.MaxRequestBodySize = 4096;

        // Custom tags on all requests
        options.CustomTags = new()
        {
            ["environment"] = "production",
            ["api"] = "github",
            ["team"] = "platform"
        };

        // Dynamic tags per request
        options.TagGenerator = req => new Dictionary<string, object>
        {
            ["endpoint"] = req.RequestUri?.AbsolutePath ?? "unknown"
        };

        // Exclude health checks from telemetry
        options.ExcludePatterns = new() { "/health", "/ready", "/live" };

        // Record errors
        options.RecordErrors = true;

        // Custom span enrichment
        options.EnrichSpan = (activity, request, response) =>
        {
            activity.SetTag("custom.tag", "value");
        };
    });

Viewing Telemetry

// OpenTelemetry setup in your app
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("my-api-client")  // Match ServiceName
        .AddJaegerExporter())
    .WithMetrics(metrics => metrics
        .AddMeter("my-api-client")
        .AddPrometheusExporter());

๐Ÿš€ All Features Combined

Use all features together with a single extension:

services.AddHttpClient<IMyApi>()
    .AddConnectorFeatures(options =>
    {
        // Enable/disable features
        options.EnableCaching = true;
        options.EnableRateLimiting = true;
        options.EnableDeduplication = true;
        options.EnableTelemetry = true;

        // Configure each feature
        options.ConfigureCaching = cache =>
        {
            cache.DefaultExpiration = TimeSpan.FromMinutes(10);
            cache.UseETag = true;
        };

        options.ConfigureRateLimiting = rate =>
        {
            rate.MaxRequestsPerSecond = 20;
            rate.MaxConcurrentRequests = 10;
        };

        options.ConfigureDeduplication = dedup =>
        {
            dedup.MaxWaitTime = TimeSpan.FromSeconds(15);
        };

        options.ConfigureTelemetry = telemetry =>
        {
            telemetry.ServiceName = "my-service";
            telemetry.RecordRequestDuration = true;
        };
    });

๐ŸŽฏ Strongly-Typed Models

Why Models Matter

โŒ The Problem with Weakly-Typed Approaches:

// Using JObject - No compile-time safety, no IntelliSense
JObject response = await httpClient.GetFromJsonAsync<JObject>("/api/users/1");
string email = response["email"]?.ToString(); // Could be null, could throw
int? age = response["age"]?.ToObject<int>(); // Runtime type conversion

// Using dynamic - Even worse, silent failures
dynamic user = await httpClient.GetFromJsonAsync("/api/users/1");
string name = user.fullName; // Typo! Should be "fullName", returns null silently
DateTime created = user.createdAt; // What if API returns string format?

โœ… The Power of Strongly-Typed Models:

// Define your model once
public class User
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("email")]  
    public string Email { get; set; } = string.Empty;

    [JsonPropertyName("full_name")]
    public string FullName { get; set; } = string.Empty;

    [JsonPropertyName("created_at")]
    public DateTime CreatedAt { get; set; }

    [JsonPropertyName("profile")]
    public UserProfile Profile { get; set; } = new();
}

public class UserProfile
{
    [JsonPropertyName("bio")]
    public string Bio { get; set; } = string.Empty;

    [JsonPropertyName("location")]
    public string Location { get; set; } = string.Empty;

    [JsonPropertyName("website")]
    public string Website { get; set; } = string.Empty;
}

// Use it everywhere with full type safety
User user = await apiClient.GetUserAsync(1);
string email = user.Email;           // โœ… Guaranteed to be string
DateTime created = user.CreatedAt;   // โœ… Guaranteed to be DateTime
string bio = user.Profile.Bio;       // โœ… Nested navigation works perfectly

Model Design Best Practices

1. Use Meaningful Names
// โŒ Poor naming
public class Data
{
    public string Val1 { get; set; }
    public int Val2 { get; set; }
}

// โœ… Descriptive naming
public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public Category Category { get; set; }
}
2. Leverage Nullable Reference Types
public class User
{
    public int Id { get; set; }                    // Required
    public string Email { get; set; } = string.Empty; // Required, default value
    public string? MiddleName { get; set; }        // Optional
    public DateTime CreatedAt { get; set; }        // Required
    public DateTime? LastLoginAt { get; set; }     // Optional
}
3. Use Composition for Complex Objects
public class Order
{
    public int Id { get; set; }
    public Customer Customer { get; set; } = new();
    public Address ShippingAddress { get; set; } = new();
    public Address BillingAddress { get; set; } = new();
    public List<OrderItem> Items { get; set; } = new();
    public PaymentInfo Payment { get; set; } = new();
}
4. Handle API Evolution
public class ApiResponse<T>
{
    [JsonPropertyName("data")]
    public T Data { get; set; } = default!;

    [JsonPropertyName("meta")]
    public ResponseMetadata Meta { get; set; } = new();
    
    // Handle new fields gracefully
    [JsonExtensionData]
    public Dictionary<string, JsonElement> AdditionalProperties { get; set; } = new();
}

๐Ÿ”„ CRUD Operations

Defining Your API Interface

using Refit;

public interface IProductApi
{
    [Get("/products")]
    Task<IEnumerable<Product>> GetProductsAsync([Query] ProductFilter? filter = null, CancellationToken cancellationToken = default);

    [Get("/products/{id}")]
    Task<Product> GetProductAsync(int id, CancellationToken cancellationToken = default);

    [Get("/categories/{categoryId}/products")]
    Task<IEnumerable<Product>> GetProductsByCategoryAsync(int categoryId, CancellationToken cancellationToken = default);

    [Post("/products")]
    Task<Product> CreateProductAsync([Body] CreateProductRequest request, CancellationToken cancellationToken = default);

    [Put("/products/{id}")]
    Task<Product> UpdateProductAsync(int id, [Body] UpdateProductRequest request, CancellationToken cancellationToken = default);

    [Delete("/products/{id}")]
    Task DeleteProductAsync(int id, CancellationToken cancellationToken = default);
}

Using the Generic Base Client

public class ProductClient : BaseApiClient<Product, int>
{
    private readonly IProductApi _api;

    public ProductClient(IProductApi api) : base(api)
    {
        _api = api;
    }

    // Add custom business logic
    public async Task<IEnumerable<Product>> GetFeaturedProductsAsync()
    {
        var allProducts = await GetAllAsync();
        return allProducts.Where(p => p.IsFeatured).Take(10);
    }

    public async Task<Product> GetProductBySkuAsync(string sku)
    {
        var products = await GetAllAsync();
        return products.FirstOrDefault(p => p.Sku == sku) 
               ?? throw new ProductNotFoundException($"Product with SKU {sku} not found");
    }
}

๐Ÿ” Advanced LINQ Querying

One of the most powerful features is the ability to query API responses using LINQ as if they were in-memory collections:

Complex Filtering and Aggregation

// Get all orders and perform complex analysis
var orders = await orderClient.GetOrdersAsync();

// Find high-value customers
var vipCustomers = orders
    .GroupBy(o => o.CustomerId)
    .Select(g => new {
        CustomerId = g.Key,
        TotalSpent = g.Sum(o => o.TotalAmount),
        OrderCount = g.Count(),
        AverageOrderValue = g.Average(o => o.TotalAmount),
        LastOrderDate = g.Max(o => o.CreatedAt)
    })
    .Where(c => c.TotalSpent > 10000 || c.OrderCount > 50)
    .OrderByDescending(c => c.TotalSpent)
    .ToList();

// Analyze product performance
var productStats = orders
    .SelectMany(o => o.Items)
    .GroupBy(item => item.ProductId)
    .Select(g => new ProductStats {
        ProductId = g.Key,
        TotalQuantitySold = g.Sum(item => item.Quantity),
        TotalRevenue = g.Sum(item => item.Price * item.Quantity),
        UniqueCustomers = g.Select(item => item.Order.CustomerId).Distinct().Count(),
        AveragePrice = g.Average(item => item.Price)
    })
    .OrderByDescending(ps => ps.TotalRevenue)
    .ToList();

Cross-Entity Relationships

// Get data from multiple endpoints
var users = await userClient.GetUsersAsync();
var posts = await postClient.GetPostsAsync();  
var comments = await commentClient.GetCommentsAsync();

// Analyze user engagement across entities
var userEngagement = users
    .Select(u => new UserEngagementReport {
        User = u,
        PostCount = posts.Count(p => p.AuthorId == u.Id),
        CommentCount = comments.Count(c => c.AuthorId == u.Id),
        PostsWithComments = posts
            .Where(p => p.AuthorId == u.Id)
            .Count(p => comments.Any(c => c.PostId == p.Id)),
        AverageCommentsPerPost = posts
            .Where(p => p.AuthorId == u.Id)
            .DefaultIfEmpty()
            .Average(p => p != null ? comments.Count(c => c.PostId == p.Id) : 0)
    })
    .Where(ue => ue.PostCount > 0) // Only active users
    .OrderByDescending(ue => ue.PostCount + ue.CommentCount)
    .ToList();

// Find trending topics
var trendingTopics = posts
    .Where(p => p.CreatedAt >= DateTime.Now.AddDays(-7)) // Last week
    .SelectMany(p => ExtractHashtags(p.Content))
    .GroupBy(hashtag => hashtag, StringComparer.OrdinalIgnoreCase)
    .Select(g => new TrendingTopic {
        Hashtag = g.Key,
        Mentions = g.Count(),
        UniqueAuthors = posts
            .Where(p => ExtractHashtags(p.Content)
                .Contains(g.Key, StringComparer.OrdinalIgnoreCase))
            .Select(p => p.AuthorId)
            .Distinct()
            .Count()
    })
    .OrderByDescending(tt => tt.Mentions)
    .Take(10)
    .ToList();

Time-Series Analysis

// Analyze sales trends over time
var salesData = await salesClient.GetSalesAsync();

var monthlySales = salesData
    .GroupBy(s => new { Year = s.Date.Year, Month = s.Date.Month })
    .Select(g => new MonthlySalesReport {
        Year = g.Key.Year,
        Month = g.Key.Month,
        TotalSales = g.Sum(s => s.Amount),
        TransactionCount = g.Count(),
        AverageTransactionValue = g.Average(s => s.Amount),
        UniqueCustomers = g.Select(s => s.CustomerId).Distinct().Count(),
        TopProduct = g.GroupBy(s => s.ProductId)
                     .OrderByDescending(pg => pg.Sum(s => s.Amount))
                     .First().Key
    })
    .OrderBy(ms => ms.Year).ThenBy(ms => ms.Month)
    .ToList();

// Calculate growth rates
for (int i = 1; i < monthlySales.Count; i++)
{
    var current = monthlySales[i];
    var previous = monthlySales[i - 1];
    
    current.GrowthRate = ((current.TotalSales - previous.TotalSales) / previous.TotalSales) * 100;
    current.CustomerGrowthRate = ((current.UniqueCustomers - previous.UniqueCustomers) / (double)previous.UniqueCustomers) * 100;
}

๐Ÿข Real-World Examples

E-Commerce Platform Integration

public class ECommerceService
{
    private readonly IProductApi _productApi;
    private readonly IOrderApi _orderApi;
    private readonly ICustomerApi _customerApi;

    public ECommerceService(IProductApi productApi, IOrderApi orderApi, ICustomerApi customerApi)
    {
        _productApi = productApi;
        _orderApi = orderApi;
        _customerApi = customerApi;
    }

    public async Task<RecommendationResult> GetPersonalizedRecommendationsAsync(int customerId)
    {
        // Get customer data and order history
        var customer = await _customerApi.GetCustomerAsync(customerId);
        var orders = await _orderApi.GetCustomerOrdersAsync(customerId);
        var allProducts = await _productApi.GetProductsAsync();

        // Analyze purchase patterns
        var purchasedProductIds = orders
            .SelectMany(o => o.Items)
            .Select(item => item.ProductId)
            .Distinct()
            .ToHashSet();

        var preferredCategories = orders
            .SelectMany(o => o.Items)
            .Join(allProducts, item => item.ProductId, product => product.Id, 
                  (item, product) => product.CategoryId)
            .GroupBy(categoryId => categoryId)
            .OrderByDescending(g => g.Count())
            .Select(g => g.Key)
            .Take(3)
            .ToList();

        // Generate recommendations
        var recommendations = allProducts
            .Where(p => !purchasedProductIds.Contains(p.Id)) // Not already purchased
            .Where(p => preferredCategories.Contains(p.CategoryId)) // In preferred categories
            .Where(p => p.Price <= customer.AverageOrderValue * 1.5) // Within price range
            .OrderByDescending(p => p.Rating)
            .ThenByDescending(p => p.SalesCount)
            .Take(10)
            .ToList();

        return new RecommendationResult
        {
            CustomerId = customerId,
            Recommendations = recommendations,
            ReasonCodes = ["category_preference", "price_range", "popularity"]
        };
    }

    public async Task<InventoryAlert[]> CheckInventoryAlertsAsync()
    {
        var products = await _productApi.GetProductsAsync();
        var recentOrders = await _orderApi.GetRecentOrdersAsync(TimeSpan.FromDays(30));

        // Calculate velocity and predict stockouts
        var alerts = products
            .Select(product => {
                var salesVelocity = recentOrders
                    .SelectMany(o => o.Items)
                    .Where(item => item.ProductId == product.Id)
                    .Sum(item => item.Quantity) / 30.0; // Daily average

                var daysToStockout = salesVelocity > 0 ? product.StockQuantity / salesVelocity : double.MaxValue;

                return new InventoryAlert
                {
                    Product = product,
                    CurrentStock = product.StockQuantity,
                    DailySalesVelocity = salesVelocity,
                    EstimatedDaysToStockout = daysToStockout,
                    AlertLevel = daysToStockout switch
                    {
                        < 7 => AlertLevel.Critical,
                        < 14 => AlertLevel.Warning,
                        < 30 => AlertLevel.Info,
                        _ => AlertLevel.None
                    }
                };
            })
            .Where(alert => alert.AlertLevel != AlertLevel.None)
            .OrderBy(alert => alert.EstimatedDaysToStockout)
            .ToArray();

        return alerts;
    }
}

Social Media Analytics

public class SocialMediaAnalyticsService
{
    private readonly IPostApi _postApi;
    private readonly IUserApi _userApi;
    private readonly IEngagementApi _engagementApi;

    public async Task<ViralContentReport> AnalyzeViralContentAsync(DateTime startDate, DateTime endDate)
    {
        var posts = await _postApi.GetPostsByDateRangeAsync(startDate, endDate);
        var engagements = await _engagementApi.GetEngagementsByDateRangeAsync(startDate, endDate);

        // Identify viral content
        var viralPosts = posts
            .Select(post => new {
                Post = post,
                Engagements = engagements.Where(e => e.PostId == post.Id),
                TotalEngagements = engagements.Where(e => e.PostId == post.Id).Sum(e => e.Count),
                EngagementRate = engagements.Where(e => e.PostId == post.Id).Sum(e => e.Count) / (double)post.ViewCount,
                ShareToViewRatio = engagements.Where(e => e.PostId == post.Id && e.Type == "share").Sum(e => e.Count) / (double)post.ViewCount
            })
            .Where(p => p.TotalEngagements > 1000 && p.EngagementRate > 0.05) // Viral thresholds
            .OrderByDescending(p => p.TotalEngagements)
            .Take(50)
            .ToList();

        // Analyze content patterns
        var contentPatterns = viralPosts
            .SelectMany(vp => ExtractContentFeatures(vp.Post))
            .GroupBy(feature => feature)
            .Select(g => new ContentPattern {
                Feature = g.Key,
                Frequency = g.Count(),
                AverageEngagement = viralPosts
                    .Where(vp => ExtractContentFeatures(vp.Post).Contains(g.Key))
                    .Average(vp => vp.TotalEngagements)
            })
            .OrderByDescending(cp => cp.AverageEngagement)
            .ToList();

        return new ViralContentReport
        {
            AnalysisPeriod = new DateRange(startDate, endDate),
            ViralPosts = viralPosts.Select(vp => vp.Post).ToList(),
            ContentPatterns = contentPatterns,
            Insights = GenerateInsights(contentPatterns)
        };
    }
}

๐Ÿ“Š Performance Benchmarks

Here's why strongly-typed models outperform dynamic alternatives:

Scenario Dynamic/JObject Strongly-Typed Performance Gain
Property Access 145 ns 12 ns 12x faster
Serialization 2,840 ns 1,120 ns 2.5x faster
Memory Usage 2,048 bytes 856 bytes 60% less memory
LINQ Queries 8,200 ns 3,100 ns 2.6x faster
Compilation Runtime errors Compile-time safety 100% error prevention

Benchmark Code Example

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class ModelPerformanceBenchmark
{
    private string _jsonData;
    private User[] _typedUsers;
    private JObject[] _dynamicUsers;

    [GlobalSetup]
    public void Setup()
    {
        _jsonData = GenerateUserJson(1000);
        _typedUsers = JsonSerializer.Deserialize<User[]>(_jsonData);
        _dynamicUsers = JsonConvert.DeserializeObject<JObject[]>(_jsonData);
    }

    [Benchmark(Baseline = true)]
    public string DynamicPropertyAccess()
    {
        return string.Join(",", _dynamicUsers.Select(u => u["email"]?.ToString()));
    }

    [Benchmark]
    public string TypedPropertyAccess()
    {
        return string.Join(",", _typedUsers.Select(u => u.Email));
    }

    [Benchmark]
    public int DynamicFiltering()
    {
        return _dynamicUsers.Count(u => u["age"]?.ToObject<int>() > 25);
    }

    [Benchmark]  
    public int TypedFiltering()
    {
        return _typedUsers.Count(u => u.Age > 25);
    }
}

๐Ÿ“– Migration Guide

From HttpClient to Strongly-Typed Client

Before: Manual HttpClient usage

public class WeaklyTypedUserService
{
    private readonly HttpClient _httpClient;

    public async Task<dynamic> GetUserAsync(int id)
    {
        var response = await _httpClient.GetStringAsync($"/api/users/{id}");
        return JsonConvert.DeserializeObject(response); // No type safety!
    }

    public async Task<List<dynamic>> SearchUsersAsync(string query)
    {
        var response = await _httpClient.GetStringAsync($"/api/users/search?q={query}");
        return JsonConvert.DeserializeObject<List<dynamic>>(response);
    }
}

After: Strongly-typed Enterprise API Client

[Headers("User-Agent: MyApp/1.0")]
public interface IUserApi
{
    [Get("/api/users/{id}")]
    Task<User> GetUserAsync(int id);

    [Get("/api/users/search")]
    Task<List<User>> SearchUsersAsync([Query] string q);

    [Post("/api/users")]
    Task<User> CreateUserAsync([Body] CreateUserRequest request);
}

public class StronglyTypedUserService
{
    private readonly IUserApi _userApi;

    public StronglyTypedUserService(IUserApi userApi)
    {
        _userApi = userApi;
    }

    public async Task<User> GetUserAsync(int id) => await _userApi.GetUserAsync(id);

    public async Task<List<User>> SearchUsersAsync(string query) => 
        await _userApi.SearchUsersAsync(query);
}

Benefits of Migration

  1. Compile-Time Safety: Catch API contract mismatches during development
  2. IntelliSense Support: Full IDE support with autocomplete and navigation
  3. Refactoring Safety: Rename properties across your entire codebase safely
  4. Performance Gains: 2-12x performance improvement in typical scenarios
  5. Maintainability: Self-documenting code with clear contracts
  6. Testing: Easy to mock and unit test with concrete interfaces

๐Ÿ› ๏ธ Best Practices

1. Model Design Principles

// โœ… Good: Immutable where possible
public record User(
    int Id,
    string Email,
    string Name,
    DateTime CreatedAt,
    UserPreferences Preferences
);

// โœ… Good: Nullable reference types for optional fields
public class UserPreferences
{
    public string Theme { get; set; } = "light";
    public string? Language { get; set; } // Optional
    public NotificationSettings Notifications { get; set; } = new();
}

// โœ… Good: Validation attributes
public class CreateUserRequest
{
    [Required, EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Required, StringLength(100, MinimumLength = 2)]
    public string Name { get; set; } = string.Empty;

    [Range(13, 120)]
    public int Age { get; set; }
}

2. Error Handling

public class RobustApiClient
{
    private readonly IApiClient _client;
    private readonly ILogger<RobustApiClient> _logger;

    public async Task<T?> SafeGetAsync<T>(int id) where T : class
    {
        try
        {
            return await _client.GetAsync<T>(id);
        }
        catch (ApiException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
        {
            _logger.LogWarning("Entity with ID {Id} not found", id);
            return null;
        }
        catch (ApiException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
        {
            _logger.LogError("Authentication failed for request");
            throw new UnauthorizedException("API authentication failed", ex);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error occurred while fetching entity {Id}", id);
            throw;
        }
    }
}

3. Caching Strategy

public class CachedApiClient : IApiClient
{
    private readonly IApiClient _innerClient;
    private readonly IMemoryCache _cache;

    public async Task<T> GetAsync<T>(int id) where T : class
    {
        var cacheKey = $"{typeof(T).Name}:{id}";
        
        if (_cache.TryGetValue(cacheKey, out T cachedValue))
        {
            return cachedValue;
        }

        var value = await _innerClient.GetAsync<T>(id);
        
        _cache.Set(cacheKey, value, TimeSpan.FromMinutes(5));
        return value;
    }
}

4. Configuration Management

public class ApiConfiguration
{
    public string BaseUrl { get; set; } = string.Empty;
    public string ApiKey { get; set; } = string.Empty;
    public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
    public int MaxRetries { get; set; } = 3;
    public bool EnableCaching { get; set; } = true;
    
    [Range(1, 100)]
    public int MaxConcurrentRequests { get; set; } = 10;
}

// In Startup.cs or Program.cs
services.Configure<ApiConfiguration>(configuration.GetSection("ApiSettings"));
services.AddConnector<IMyApi, MyEntity, int>((serviceProvider, options) =>
{
    var config = serviceProvider.GetRequiredService<IOptions<ApiConfiguration>>().Value;
    options.BaseUrl = config.BaseUrl;
    options.Timeout = config.Timeout;
});

๐Ÿงช Testing Strategies

Unit Testing with Strongly-Typed Models

public class UserServiceTests
{
    [Test]
    public async Task GetActiveUsers_ReturnsOnlyActiveUsers()
    {
        // Arrange
        var mockApi = new Mock<IUserApi>();
        var testUsers = new List<User>
        {
            new() { Id = 1, Name = "Active User", IsActive = true },
            new() { Id = 2, Name = "Inactive User", IsActive = false }
        };
        
        mockApi.Setup(x => x.GetUsersAsync(It.IsAny<CancellationToken>()))
               .ReturnsAsync(testUsers);
        
        var service = new UserService(mockApi.Object);
        
        // Act
        var result = await service.GetActiveUsersAsync();
        
        // Assert
        Assert.That(result, Has.Count.EqualTo(1));
        Assert.That(result.First().Name, Is.EqualTo("Active User"));
    }
}

Integration Testing

[TestFixture]
public class JsonPlaceholderIntegrationTests
{
    private ServiceProvider _serviceProvider;
    private IJsonPlaceholderApi _api;

    [OneTimeSetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddConnector<IJsonPlaceholderApi, User, int>(options =>
        {
            options.BaseUrl = "https://jsonplaceholder.typicode.com";
        }, new TokenAuthenticationProvider("no-auth-required"));

        _serviceProvider = services.BuildServiceProvider();
        _api = _serviceProvider.GetRequiredService<IJsonPlaceholderApi>();
    }

    [Test]
    public async Task GetUsers_ReturnsStronglyTypedUsers()
    {
        var users = await _api.GetUsersAsync();
        
        Assert.That(users, Is.Not.Empty);
        Assert.That(users.First().Email, Is.Not.Empty);
        Assert.That(users.First().Address.City, Is.Not.Empty);
    }
}

๐Ÿ”ง Advanced Configuration

Custom Serialization Settings

services.AddConnector<IMyApi, MyEntity, int>(options =>
{
    options.BaseUrl = "https://api.example.com";
    options.SerializerOptions = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        NumberHandling = JsonNumberHandling.AllowReadingFromString,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };
});

Request/Response Interceptors

public class LoggingHandler : DelegatingHandler
{
    private readonly ILogger<LoggingHandler> _logger;

    public LoggingHandler(ILogger<LoggingHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Sending {Method} request to {Uri}", 
            request.Method, request.RequestUri);

        var response = await base.SendAsync(request, cancellationToken);

        _logger.LogInformation("Received {StatusCode} response from {Uri}", 
            response.StatusCode, request.RequestUri);

        return response;
    }
}

// Register in DI
services.AddTransient<LoggingHandler>();
services.AddHttpClient<IMyApi>()
    .AddHttpMessageHandler<LoggingHandler>();

๐Ÿ“ Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

  1. Clone the repository

    git clone https://github.com/noundry/Connector.git
    cd Connector
    
  2. Install dependencies

    dotnet restore
    
  3. Run tests

    dotnet test
    
  4. Build the project

    dotnet build
    

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿค Support

๐Ÿ† Acknowledgments

  • Built with Refit - The automatic type-safe REST library
  • Inspired by enterprise-grade API client patterns
  • Thanks to all contributors

Made with โค๏ธ by the Noundry team

Empowering developers to build better APIs with strongly-typed, maintainable, and performant client libraries.

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

This package has no dependencies.

Version Downloads Last Updated
1.0.0 195 3/4/2026