Noundry.Connector.Cli
1.0.0
dotnet tool install --global Noundry.Connector.Cli --version 1.0.0
dotnet new tool-manifest
dotnet tool install --local Noundry.Connector.Cli --version 1.0.0
#tool dotnet:?package=Noundry.Connector.Cli&version=1.0.0
nuke :add-package Noundry.Connector.Cli --version 1.0.0
Noundry.Connector
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
- ๐ฎ CLI Tool Walkthrough
- Quick Start
- Installation
- Authentication
- Automatic Pagination
- Smart Caching
- Rate Limiting
- Request Deduplication
- OpenTelemetry
- Strongly-Typed Models
- CRUD Operations
- LINQ Querying
- Real-World Examples
- Best Practices
- Performance Benchmarks
- 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
Option 1: CLI Generator (Recommended)
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
- Compile-Time Safety: Catch API contract mismatches during development
- IntelliSense Support: Full IDE support with autocomplete and navigation
- Refactoring Safety: Rename properties across your entire codebase safely
- Performance Gains: 2-12x performance improvement in typical scenarios
- Maintainability: Self-documenting code with clear contracts
- 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
Clone the repository
git clone https://github.com/noundry/Connector.git cd ConnectorInstall dependencies
dotnet restoreRun tests
dotnet testBuild the project
dotnet build
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ค Support
- Documentation: Full Documentation
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: support@noundry.com
๐ 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 | Versions 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. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 195 | 3/4/2026 |