From cc5540ee3048921f694ade32e3850d987e30829b Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Mon, 24 Nov 2025 19:47:46 +0100 Subject: [PATCH 01/14] feat: enhance integration test workflow with quick and slow test categories --- CLAUDE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e69de29b From 755bbd8a4f74a417d611aac307b1cbc1de278b96 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sun, 23 Nov 2025 22:50:16 +0000 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20Add=20async=20initialization=20pa?= =?UTF-8?q?ttern=20and=20dependency=20injection=20support=20Implements=20a?= =?UTF-8?q?sync=20initialization=20pattern=20with=20IHostedService=20for?= =?UTF-8?q?=20proper=20DI=20integration=20without=20blocking=20calls=20or?= =?UTF-8?q?=20GetAwaiter().GetResult().=20Key=20Features:=20-=20Async=20in?= =?UTF-8?q?itialization=20using=20Lazy=20pattern=20-=20Eager=20initi?= =?UTF-8?q?alization=20via=20IHostedService=20(default)=20-=20Lazy=20initi?= =?UTF-8?q?alization=20option=20for=20on-demand=20scenarios=20-=20Full=20b?= =?UTF-8?q?ackward=20compatibility=20with=20Connect.Local/Cloud=20helpers?= =?UTF-8?q?=20-=20Follows=20existing=20REST=20=E2=86=92=20Meta=20=E2=86=92?= =?UTF-8?q?=20gRPC=20initialization=20flow=20New=20Components:=20-=20Weavi?= =?UTF-8?q?ateOptions:=20Configuration=20class=20for=20DI=20scenarios=20-?= =?UTF-8?q?=20WeaviateServiceCollectionExtensions:=20AddWeaviate()=20exten?= =?UTF-8?q?sion=20methods=20-=20WeaviateInitializationService:=20IHostedSe?= =?UTF-8?q?rvice=20for=20eager=20initialization=20-=20DI-friendly=20constr?= =?UTF-8?q?uctor:=20WeaviateClient(IOptions)=20Public=20A?= =?UTF-8?q?PI=20Changes:=20-=20Added:=20WeaviateClient.InitializeAsync()?= =?UTF-8?q?=20-=20manually=20trigger=20initialization=20-=20Added:=20Weavi?= =?UTF-8?q?ateClient.IsInitialized=20-=20check=20initialization=20status?= =?UTF-8?q?=20-=20Added:=20EnsureInitializedAsync()=20-=20internal=20helpe?= =?UTF-8?q?r=20for=20async=20methods=20-=20Modified:=20GetMeta(),=20Live()?= =?UTF-8?q?,=20IsReady()=20-=20now=20await=20initialization=20Dependencies?= =?UTF-8?q?=20Added:=20-=20Microsoft.Extensions.Hosting.Abstractions=209.0?= =?UTF-8?q?.8=20-=20Microsoft.Extensions.Options=209.0.8=20Documentation:?= =?UTF-8?q?=20-=20DEPENDENCY=5FINJECTION.md=20-=20comprehensive=20DI=20usa?= =?UTF-8?q?ge=20guide=20-=20DependencyInjectionExample.cs=20-=20example=20?= =?UTF-8?q?demonstrating=20all=20patterns=20Benefits:=20=E2=9C=85=20No=20m?= =?UTF-8?q?ore=20GetAwaiter().GetResult()=20anti-pattern=20=E2=9C=85=20Wor?= =?UTF-8?q?ks=20seamlessly=20with=20ASP.NET=20Core=20DI=20=E2=9C=85=20Thre?= =?UTF-8?q?ad-safe=20initialization=20with=20Lazy=20=E2=9C=85=20Keep?= =?UTF-8?q?s=20REST=20=E2=86=92=20Meta=20=E2=86=92=20gRPC=20flow=20for=20m?= =?UTF-8?q?ax=20message=20size=20=E2=9C=85=20Connect.Local()=20and=20Conne?= =?UTF-8?q?ct.Cloud()=20still=20work=20unchanged=20=E2=9C=85=20Proper=20as?= =?UTF-8?q?ync/await=20throughout=20Breaking=20Changes:=20None=20-=20All?= =?UTF-8?q?=20existing=20constructors=20and=20patterns=20continue=20to=20w?= =?UTF-8?q?ork=20-=20Connect=20helpers=20unchanged=20-=20WeaviateClientBui?= =?UTF-8?q?lder=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEPENDENCY_INJECTION.md | 464 ++++++++++++++++++ src/Example/DependencyInjectionExample.cs | 207 ++++++++ .../WeaviateInitializationService.cs | 49 ++ .../DependencyInjection/WeaviateOptions.cs | 100 ++++ .../WeaviateServiceCollectionExtensions.cs | 117 +++++ src/Weaviate.Client/Weaviate.Client.csproj | 2 + src/Weaviate.Client/WeaviateClient.cs | 129 ++++- 7 files changed, 1064 insertions(+), 4 deletions(-) create mode 100644 DEPENDENCY_INJECTION.md create mode 100644 src/Example/DependencyInjectionExample.cs create mode 100644 src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs create mode 100644 src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs create mode 100644 src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs diff --git a/DEPENDENCY_INJECTION.md b/DEPENDENCY_INJECTION.md new file mode 100644 index 00000000..2f27e3df --- /dev/null +++ b/DEPENDENCY_INJECTION.md @@ -0,0 +1,464 @@ +# Dependency Injection with Weaviate Client + +The Weaviate C# client now supports modern dependency injection patterns with async initialization. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Configuration Options](#configuration-options) +- [Eager vs Lazy Initialization](#eager-vs-lazy-initialization) +- [Using Connect Helpers](#using-connect-helpers) +- [Advanced Scenarios](#advanced-scenarios) + +--- + +## Quick Start + +### 1. Register Weaviate in `Program.cs` + +```csharp +using Weaviate.Client.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +// Option 1: Configure inline +builder.Services.AddWeaviate(options => +{ + options.RestEndpoint = "localhost"; + options.GrpcEndpoint = "localhost"; + options.RestPort = 8080; + options.GrpcPort = 50051; +}); + +// Option 2: From appsettings.json +builder.Services.AddWeaviate(builder.Configuration.GetSection("Weaviate")); + +// Option 3: Helper methods for common scenarios +builder.Services.AddWeaviateLocal(); // Connects to localhost:8080 +builder.Services.AddWeaviateCloud("my-cluster.weaviate.cloud", "api-key-here"); + +var app = builder.Build(); +``` + +### 2. Configure in `appsettings.json` (Optional) + +```json +{ + "Weaviate": { + "RestEndpoint": "localhost", + "GrpcEndpoint": "localhost", + "RestPort": 8080, + "GrpcPort": 50051, + "UseSsl": false, + "DefaultTimeout": "00:00:30", + "InitTimeout": "00:00:02", + "DataTimeout": "00:02:00", + "QueryTimeout": "00:01:00" + } +} +``` + +### 3. Inject and Use in Your Services + +```csharp +public class CatService +{ + private readonly WeaviateClient _weaviate; + + public CatService(WeaviateClient weaviate) + { + _weaviate = weaviate; + // Client is already initialized and ready to use! + } + + public async Task> SearchCatsAsync(string query) + { + var collection = _weaviate.Collections.Use("Cat"); + + var results = await collection.Query.NearText(new NearTextOptions + { + Text = query, + Limit = 10 + }); + + return results.Objects.Select(o => o.As()!).ToList(); + } + + public async Task AddCatAsync(Cat cat) + { + var collection = _weaviate.Collections.Use("Cat"); + return await collection.Data.Insert(cat); + } +} +``` + +--- + +## Configuration Options + +### WeaviateOptions Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `RestEndpoint` | `string` | `"localhost"` | REST API endpoint address | +| `RestPort` | `ushort` | `8080` | REST API port | +| `GrpcEndpoint` | `string` | `"localhost"` | gRPC endpoint address | +| `GrpcPort` | `ushort` | `50051` | gRPC port | +| `UseSsl` | `bool` | `false` | Whether to use SSL/TLS | +| `Credentials` | `ICredentials?` | `null` | Authentication credentials | +| `DefaultTimeout` | `TimeSpan?` | `30s` | Default timeout for all operations | +| `InitTimeout` | `TimeSpan?` | `2s` | Timeout for initialization | +| `DataTimeout` | `TimeSpan?` | `120s` | Timeout for data operations | +| `QueryTimeout` | `TimeSpan?` | `60s` | Timeout for query operations | +| `Headers` | `Dictionary?` | `null` | Additional HTTP headers | +| `RetryPolicy` | `RetryPolicy?` | Default | Retry policy for failed requests | + +### Authentication + +```csharp +builder.Services.AddWeaviate(options => +{ + options.RestEndpoint = "my-cluster.weaviate.cloud"; + options.GrpcEndpoint = "grpc-my-cluster.weaviate.cloud"; + options.UseSsl = true; + + // API Key authentication + options.Credentials = Auth.ApiKey("your-api-key"); + + // Or OAuth2 Client Credentials + options.Credentials = Auth.ClientCredentials("client-secret", "scope1", "scope2"); + + // Or OAuth2 Password Flow + options.Credentials = Auth.ClientPassword("username", "password", "scope1"); +}); +``` + +--- + +## Eager vs Lazy Initialization + +### Eager Initialization (Default - Recommended) + +The client initializes during application startup before handling requests. + +```csharp +// Eager initialization is enabled by default +builder.Services.AddWeaviate(options => { ... }); + +// Explicitly enable +builder.Services.AddWeaviate(options => { ... }, eagerInitialization: true); +``` + +**Benefits:** +- ✅ Client is ready when first request arrives +- ✅ Fails fast if connection issues exist +- ✅ Simpler usage - no need to await initialization + +**How it works:** +1. `WeaviateClient` is constructed with DI +2. `IHostedService` runs on app startup +3. Calls `client.InitializeAsync()` which: + - Creates REST client + - Fetches server metadata + - Creates gRPC client with correct max message size +4. Client is fully initialized before app accepts requests + +### Lazy Initialization + +The client initializes on first use. + +```csharp +builder.Services.AddWeaviate(options => { ... }, eagerInitialization: false); +``` + +**When to use:** +- Application startup time is critical +- Weaviate connection isn't needed immediately +- You want to handle connection failures gracefully + +**Usage with lazy initialization:** +```csharp +public class MyService +{ + private readonly WeaviateClient _client; + + public MyService(WeaviateClient client) + { + _client = client; + } + + public async Task DoWorkAsync() + { + // Manually ensure initialization + if (!_client.IsInitialized) + { + await _client.InitializeAsync(); + } + + // Now use the client + var collections = _client.Collections.Use("Cat"); + // ... + } +} +``` + +--- + +## Using Connect Helpers + +The `Connect.Local()` and `Connect.Cloud()` helpers **still work** and are compatible with the new async pattern! + +```csharp +// These work exactly as before - fully async, no blocking +var client = await Connect.Local(); +var client = await Connect.Cloud("my-cluster.weaviate.cloud", "api-key"); + +// With timeouts +var client = await Connect.Local( + hostname: "localhost", + defaultTimeout: TimeSpan.FromSeconds(60), + queryTimeout: TimeSpan.FromSeconds(30) +); +``` + +These methods: +- ✅ Return a fully initialized client +- ✅ Use async initialization (no blocking) +- ✅ Follow the same REST → Meta → gRPC initialization flow +- ✅ Are perfect for console apps, scripts, and testing + +--- + +## Advanced Scenarios + +### Custom HttpMessageHandler + +```csharp +builder.Services.AddWeaviate(options => +{ + options.RestEndpoint = "localhost"; + // Note: CustomHandlers property exists on ClientConfiguration, not WeaviateOptions + // For now, use WeaviateClientBuilder for custom handlers +}); + +// Alternative: Build client manually +builder.Services.AddSingleton(sp => +{ + var client = await WeaviateClientBuilder + .Local() + .WithHttpMessageHandler(myCustomHandler) + .BuildAsync(); + + return client; +}); +``` + +### Check Initialization Status + +```csharp +public class MyService +{ + private readonly WeaviateClient _client; + + public MyService(WeaviateClient client) + { + _client = client; + } + + public async Task GetStatusAsync() + { + if (!_client.IsInitialized) + { + return "Client is initializing..."; + } + + var version = _client.WeaviateVersion; + return $"Connected to Weaviate {version}"; + } +} +``` + +### Manual Initialization + +```csharp +// Create client without DI +var options = Options.Create(new WeaviateOptions +{ + RestEndpoint = "localhost" +}); + +var client = new WeaviateClient(options); + +// Manually initialize +await client.InitializeAsync(); + +// Now use the client +var collection = client.Collections.Use("Cat"); +``` + +### Integration Testing + +```csharp +public class MyIntegrationTests : IAsyncLifetime +{ + private WeaviateClient _client = null!; + + public async Task InitializeAsync() + { + // Setup test client + _client = await Connect.Local(); + + // Verify it's ready + Assert.True(_client.IsInitialized); + Assert.True(await _client.IsReady()); + } + + [Fact] + public async Task CanSearchCats() + { + var collection = _client.Collections.Use("Cat"); + var results = await collection.Query.FetchObjects(limit: 10); + + Assert.NotEmpty(results.Objects); + } + + public async Task DisposeAsync() + { + _client?.Dispose(); + } +} +``` + +--- + +## How It Works + +### Initialization Flow + +1. **Constructor** (`WeaviateClient(IOptions)`) + - Creates a `Lazy` for async initialization + - Sets up sub-clients (Collections, Cluster, etc.) + - Returns immediately + +2. **First Async Method Call** or **IHostedService** + - Triggers `EnsureInitializedAsync()` + - Runs initialization task (only once, thread-safe) + +3. **Initialization Task** + ``` + ┌─────────────────────────────────────────┐ + │ 1. Initialize Token Service (OAuth, etc)│ + ├─────────────────────────────────────────┤ + │ 2. Create REST Client │ + ├─────────────────────────────────────────┤ + │ 3. Fetch Metadata from REST /v1/meta │ + │ - Get GrpcMaxMessageSize │ + │ - Get Server Version │ + ├─────────────────────────────────────────┤ + │ 4. Create gRPC Client │ + │ - Use max message size from Meta │ + ├─────────────────────────────────────────┤ + │ 5. Update Cluster Client │ + └─────────────────────────────────────────┘ + ``` + +4. **Subsequent Calls** + - `EnsureInitializedAsync()` returns immediately (already initialized) + - No performance penalty + +### No More `GetAwaiter().GetResult()`! + +The old pattern had to block: +```csharp +// ❌ Old: Blocking async call in constructor +var client = new WeaviateClient(config); +var meta = GetMetaAsync().GetAwaiter().GetResult(); // Deadlock risk! +``` + +The new pattern is fully async: +```csharp +// ✅ New: Lazy async initialization +var client = new WeaviateClient(options); +// Later, when needed... +await client.InitializeAsync(); // Or called automatically +``` + +--- + +## Migration Guide + +### From Old Constructor Pattern + +**Before:** +```csharp +var config = new ClientConfiguration +{ + RestAddress = "localhost", + RestPort = 8080 +}; + +var client = new WeaviateClient(config); +``` + +**After (DI):** +```csharp +builder.Services.AddWeaviate(options => +{ + options.RestEndpoint = "localhost"; + options.RestPort = 8080; +}); + +// In your service +public MyService(WeaviateClient client) { ... } +``` + +**After (Non-DI):** +```csharp +var client = await Connect.Local(); +// Or +var options = Options.Create(new WeaviateOptions { ... }); +var client = new WeaviateClient(options); +await client.InitializeAsync(); +``` + +### From `WeaviateClientBuilder` + +**Before:** +```csharp +var client = await WeaviateClientBuilder + .Local() + .WithCredentials(Auth.ApiKey("key")) + .BuildAsync(); +``` + +**After (still works!):** +```csharp +// This pattern still works exactly as before +var client = await WeaviateClientBuilder + .Local() + .WithCredentials(Auth.ApiKey("key")) + .BuildAsync(); +``` + +**Or with DI:** +```csharp +builder.Services.AddWeaviate(options => +{ + options.Credentials = Auth.ApiKey("key"); +}); +``` + +--- + +## Summary + +✅ **Fully async** - No blocking calls in constructors +✅ **DI-friendly** - Works seamlessly with ASP.NET Core and other DI containers +✅ **Eager initialization** - Ready before first request (with IHostedService) +✅ **Lazy initialization** - Initialize on demand if needed +✅ **Backward compatible** - `Connect.Local()`, `Connect.Cloud()`, and `WeaviateClientBuilder` still work +✅ **Same initialization flow** - REST → Meta → gRPC with max message size +✅ **Thread-safe** - Uses `Lazy` pattern +✅ **Testable** - Easy to mock and test + +For more information, see the [main README](README.md) and [examples](src/Example/). diff --git a/src/Example/DependencyInjectionExample.cs b/src/Example/DependencyInjectionExample.cs new file mode 100644 index 00000000..d4b101b9 --- /dev/null +++ b/src/Example/DependencyInjectionExample.cs @@ -0,0 +1,207 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Weaviate.Client; +using Weaviate.Client.DependencyInjection; + +namespace Example; + +/// +/// Example demonstrating how to use Weaviate with dependency injection. +/// +public class DependencyInjectionExample +{ + public static async Task Main(string[] args) + { + // Build host with dependency injection + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Register Weaviate client + services.AddWeaviateLocal( + hostname: "localhost", + restPort: 8080, + grpcPort: 50051, + eagerInitialization: true // Client initializes on startup + ); + + // Register your services that use Weaviate + services.AddSingleton(); + }) + .Build(); + + // Run the host - this triggers eager initialization + await host.StartAsync(); + + // Get the service and use it + var catService = host.Services.GetRequiredService(); + + Console.WriteLine("=== Dependency Injection Example ===\n"); + + // The client is already initialized and ready to use! + await catService.DemonstrateUsageAsync(); + + await host.StopAsync(); + } +} + +/// +/// Example service that uses Weaviate via dependency injection. +/// +public class CatService +{ + private readonly WeaviateClient _weaviate; + private readonly ILogger _logger; + + public CatService(WeaviateClient weaviate, ILogger logger) + { + _weaviate = weaviate; + _logger = logger; + + // Client is already initialized! + _logger.LogInformation( + "CatService created. Weaviate version: {Version}", + _weaviate.WeaviateVersion); + } + + public async Task DemonstrateUsageAsync() + { + // Check if client is initialized + _logger.LogInformation("Client initialized: {IsInitialized}", _weaviate.IsInitialized); + + // Create or get collection + var collection = _weaviate.Collections.Use("Cat"); + + try + { + // Check if collection exists + var config = await collection.Config.Get(); + _logger.LogInformation("Collection 'Cat' already exists"); + } + catch + { + // Create collection + _logger.LogInformation("Creating Cat collection..."); + await _weaviate.Collections.Create(new Weaviate.Client.Models.CollectionConfig + { + Name = "Cat", + Description = "Example cat collection for DI demo", + Properties = Weaviate.Client.Models.Property.FromClass(), + VectorConfig = new Weaviate.Client.Models.VectorConfig( + "default", + new Weaviate.Client.Models.Vectorizer.Text2VecWeaviate()) + }); + } + + // Insert a cat + _logger.LogInformation("Inserting a cat..."); + var catId = await collection.Data.Insert(new Cat + { + Name = "Fluffy", + Breed = "Persian", + Color = "white", + Counter = 1 + }); + + _logger.LogInformation("Inserted cat with ID: {Id}", catId); + + // Query cats + _logger.LogInformation("Querying cats..."); + var results = await collection.Query.FetchObjects(limit: 10); + + _logger.LogInformation("Found {Count} cats", results.Objects.Count()); + + foreach (var obj in results.Objects) + { + var cat = obj.As(); + _logger.LogInformation(" - {Name} ({Breed}, {Color})", cat?.Name, cat?.Breed, cat?.Color); + } + + // Cleanup + _logger.LogInformation("Cleaning up..."); + await collection.Delete(); + } +} + +/// +/// Alternative example using configuration from appsettings.json +/// +public class ConfigurationExample +{ + public static async Task RunAsync() + { + var host = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + // Register from configuration section + services.AddWeaviate( + context.Configuration.GetSection("Weaviate"), + eagerInitialization: true + ); + + services.AddSingleton(); + }) + .Build(); + + await host.StartAsync(); + + var catService = host.Services.GetRequiredService(); + await catService.DemonstrateUsageAsync(); + + await host.StopAsync(); + } +} + +/// +/// Example using lazy initialization +/// +public class LazyInitializationExample +{ + public static async Task RunAsync() + { + var host = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + // Lazy initialization - client initializes on first use + services.AddWeaviateLocal(eagerInitialization: false); + }) + .Build(); + + await host.StartAsync(); + + var client = host.Services.GetRequiredService(); + + Console.WriteLine($"Is initialized: {client.IsInitialized}"); // False + + // Manually trigger initialization + await client.InitializeAsync(); + + Console.WriteLine($"Is initialized: {client.IsInitialized}"); // True + Console.WriteLine($"Weaviate version: {client.WeaviateVersion}"); + + await host.StopAsync(); + } +} + +/// +/// Example using Connect helpers (backward compatible) +/// +public class ConnectHelperExample +{ + public static async Task RunAsync() + { + // These still work! Fully async, no blocking + var client = await Connect.Local(); + + Console.WriteLine($"Connected to Weaviate {client.WeaviateVersion}"); + + var collection = client.Collections.Use("Cat"); + + // Use the client... + var results = await collection.Query.FetchObjects(limit: 10); + + Console.WriteLine($"Found {results.Objects.Count()} cats"); + + client.Dispose(); + } +} diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs b/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs new file mode 100644 index 00000000..6fb143b9 --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Weaviate.Client.DependencyInjection; + +/// +/// Background service that eagerly initializes the Weaviate client during application startup. +/// This ensures the client is ready to use when injected into other services. +/// +internal class WeaviateInitializationService : IHostedService +{ + private readonly WeaviateClient _client; + private readonly ILogger _logger; + + public WeaviateInitializationService( + WeaviateClient client, + ILogger logger) + { + _client = client; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Initializing Weaviate client..."); + + try + { + // Trigger async initialization + await _client.InitializeAsync(cancellationToken); + + var version = _client.WeaviateVersion; + _logger.LogInformation( + "Weaviate client initialized successfully. Server version: {Version}", + version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to initialize Weaviate client"); + throw; // Fail startup if client can't connect + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping Weaviate client initialization service"); + return Task.CompletedTask; + } +} diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs new file mode 100644 index 00000000..dfc64b4a --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs @@ -0,0 +1,100 @@ +namespace Weaviate.Client.DependencyInjection; + +/// +/// Configuration options for Weaviate client when using dependency injection. +/// +public class WeaviateOptions +{ + /// + /// REST endpoint address. Default is "localhost". + /// + public string RestEndpoint { get; set; } = "localhost"; + + /// + /// REST API path. Default is "v1/". + /// + public string RestPath { get; set; } = "v1/"; + + /// + /// gRPC endpoint address. Default is "localhost". + /// + public string GrpcEndpoint { get; set; } = "localhost"; + + /// + /// gRPC path. Default is empty. + /// + public string GrpcPath { get; set; } = ""; + + /// + /// REST port. Default is 8080. + /// + public ushort RestPort { get; set; } = 8080; + + /// + /// gRPC port. Default is 50051. + /// + public ushort GrpcPort { get; set; } = 50051; + + /// + /// Whether to use SSL/TLS. Default is false. + /// + public bool UseSsl { get; set; } = false; + + /// + /// Additional HTTP headers to include in requests. + /// + public Dictionary? Headers { get; set; } + + /// + /// Authentication credentials. + /// + public ICredentials? Credentials { get; set; } + + /// + /// Default timeout for all operations. + /// + public TimeSpan? DefaultTimeout { get; set; } + + /// + /// Timeout for initialization operations (GetMeta, Live, IsReady). + /// + public TimeSpan? InitTimeout { get; set; } + + /// + /// Timeout for data operations (Insert, Delete, Update, Reference management). + /// + public TimeSpan? DataTimeout { get; set; } + + /// + /// Timeout for query/search operations (FetchObjects, NearText, BM25, Hybrid, etc.). + /// + public TimeSpan? QueryTimeout { get; set; } + + /// + /// Retry policy for failed requests. + /// + public RetryPolicy? RetryPolicy { get; set; } + + /// + /// Converts these options to a ClientConfiguration. + /// + internal ClientConfiguration ToClientConfiguration() + { + return new ClientConfiguration( + RestAddress: RestEndpoint, + RestPath: RestPath, + GrpcAddress: GrpcEndpoint, + GrpcPath: GrpcPath, + RestPort: RestPort, + GrpcPort: GrpcPort, + UseSsl: UseSsl, + Headers: Headers, + Credentials: Credentials, + DefaultTimeout: DefaultTimeout, + InitTimeout: InitTimeout, + DataTimeout: DataTimeout, + QueryTimeout: QueryTimeout, + RetryPolicy: RetryPolicy + ); + } +} diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs new file mode 100644 index 00000000..a70e3c05 --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Weaviate.Client.DependencyInjection; + +/// +/// Extension methods for registering Weaviate services with dependency injection. +/// +public static class WeaviateServiceCollectionExtensions +{ + /// + /// Adds Weaviate client services to the dependency injection container. + /// + /// The service collection. + /// Action to configure Weaviate options. + /// Whether to initialize the client eagerly on application startup. Default is true. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviate( + this IServiceCollection services, + Action configureOptions, + bool eagerInitialization = true) + { + services.Configure(configureOptions); + services.AddSingleton(); + + if (eagerInitialization) + { + services.AddHostedService(); + } + + return services; + } + + /// + /// Adds Weaviate client services to the dependency injection container using configuration. + /// + /// The service collection. + /// The configuration section containing Weaviate settings. + /// Whether to initialize the client eagerly on application startup. Default is true. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviate( + this IServiceCollection services, + IConfiguration configuration, + bool eagerInitialization = true) + { + services.Configure(configuration); + services.AddSingleton(); + + if (eagerInitialization) + { + services.AddHostedService(); + } + + return services; + } + + /// + /// Adds Weaviate client services with connection helpers. + /// + /// The service collection. + /// Hostname for local Weaviate instance. Default is "localhost". + /// REST port. Default is 8080. + /// gRPC port. Default is 50051. + /// Whether to use SSL/TLS. Default is false. + /// Authentication credentials. + /// Whether to initialize the client eagerly on application startup. Default is true. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateLocal( + this IServiceCollection services, + string hostname = "localhost", + ushort restPort = 8080, + ushort grpcPort = 50051, + bool useSsl = false, + ICredentials? credentials = null, + bool eagerInitialization = true) + { + services.AddWeaviate(options => + { + options.RestEndpoint = hostname; + options.GrpcEndpoint = hostname; + options.RestPort = restPort; + options.GrpcPort = grpcPort; + options.UseSsl = useSsl; + options.Credentials = credentials; + }, eagerInitialization); + + return services; + } + + /// + /// Adds Weaviate client services configured for Weaviate Cloud. + /// + /// The service collection. + /// The Weaviate Cloud cluster endpoint (e.g., "my-cluster.weaviate.cloud"). + /// API key for authentication. + /// Whether to initialize the client eagerly on application startup. Default is true. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateCloud( + this IServiceCollection services, + string clusterEndpoint, + string? apiKey = null, + bool eagerInitialization = true) + { + services.AddWeaviate(options => + { + options.RestEndpoint = clusterEndpoint; + options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); + }, eagerInitialization); + + return services; + } +} diff --git a/src/Weaviate.Client/Weaviate.Client.csproj b/src/Weaviate.Client/Weaviate.Client.csproj index b1c41b3b..f3fbf743 100644 --- a/src/Weaviate.Client/Weaviate.Client.csproj +++ b/src/Weaviate.Client/Weaviate.Client.csproj @@ -13,8 +13,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index dc5f5585..cd7d33ee 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Weaviate.Client.Grpc; using Weaviate.Client.Rest; @@ -348,8 +349,13 @@ public partial class WeaviateClient : IDisposable } ); + // Async initialization support + private readonly Lazy? _initializationTask; + private readonly ClientConfiguration? _configForAsyncInit; + public async Task GetMeta(CancellationToken cancellationToken = default) { + await EnsureInitializedAsync(); var meta = await RestClient.GetMeta(CreateInitCancellationToken(cancellationToken)); return new Models.MetaInfo @@ -381,17 +387,19 @@ public partial class WeaviateClient : IDisposable /// /// Returns true if the Weaviate process is live. /// - public Task Live(CancellationToken cancellationToken = default) + public async Task Live(CancellationToken cancellationToken = default) { - return RestClient.LiveAsync(CreateInitCancellationToken(cancellationToken)); + await EnsureInitializedAsync(); + return await RestClient.LiveAsync(CreateInitCancellationToken(cancellationToken)); } /// /// Returns true if the Weaviate instance is ready to accept requests. /// - public Task IsReady(CancellationToken cancellationToken = default) + public async Task IsReady(CancellationToken cancellationToken = default) { - return RestClient.ReadyAsync(CreateInitCancellationToken(cancellationToken)); + await EnsureInitializedAsync(); + return await RestClient.ReadyAsync(CreateInitCancellationToken(cancellationToken)); } /// @@ -557,6 +565,119 @@ internal WeaviateClient( Groups = new GroupsClient(this); } + /// + /// Constructor for dependency injection scenarios. + /// Uses async initialization pattern - call InitializeAsync() or ensure IHostedService runs. + /// + public WeaviateClient(IOptions options) + : this(options, null) + { + } + + /// + /// Constructor for dependency injection scenarios with logger. + /// Uses async initialization pattern - call InitializeAsync() or ensure IHostedService runs. + /// + public WeaviateClient( + IOptions options, + ILogger? logger) + { + var weaviateOptions = options.Value; + _configForAsyncInit = weaviateOptions.ToClientConfiguration(); + Configuration = _configForAsyncInit; + _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + // Initialize Lazy task that will run initialization on first access + _initializationTask = new Lazy(() => PerformInitializationAsync(_configForAsyncInit)); + + // Initialize sub-clients - they will be properly set up after async initialization + Collections = new CollectionsClient(this); + Cluster = new ClusterClient(null!); // Will be set after init + Alias = new AliasClient(this); + Users = new UsersClient(this); + Roles = new RolesClient(this); + Groups = new GroupsClient(this); + } + + /// + /// Performs the actual async initialization. + /// This follows the same flow as ClientConfiguration.BuildAsync(). + /// + private async Task PerformInitializationAsync(ClientConfiguration config) + { + _logger.LogDebug("Starting Weaviate client initialization..."); + + // Initialize token service asynchronously + var tokenService = await ClientConfiguration.InitializeTokenService(config); + + // Create REST client + RestClient = CreateRestClient(config, null, tokenService, _logger); + + // Fetch metadata eagerly with init timeout - this will throw if authentication fails + var initTimeout = config.InitTimeout ?? config.DefaultTimeout ?? WeaviateDefaults.DefaultTimeout; + var metaCts = new CancellationTokenSource(initTimeout); + var metaDto = await RestClient.GetMeta(metaCts.Token); + _metaCache = new Models.MetaInfo + { + GrpcMaxMessageSize = metaDto?.GrpcMaxMessageSize is not null + ? Convert.ToUInt64(metaDto.GrpcMaxMessageSize) + : null, + Hostname = metaDto?.Hostname ?? string.Empty, + Version = + Models.MetaInfo.ParseWeaviateVersion(metaDto?.Version ?? string.Empty) + ?? new System.Version(0, 0), + Modules = metaDto?.Modules?.ToDictionary() ?? [], + }; + + var maxMessageSize = _metaCache.GrpcMaxMessageSize; + + // Create gRPC client with metadata + GrpcClient = CreateGrpcClient(config, tokenService, maxMessageSize); + + // Update Cluster client with the REST client + Cluster = new ClusterClient(RestClient); + + _logger.LogDebug("Weaviate client initialization completed"); + } + + /// + /// Explicitly initializes the client asynchronously. + /// This is called automatically by IHostedService when using DI with eager initialization. + /// Can be called manually for lazy initialization scenarios. + /// + /// Cancellation token. + /// A task representing the initialization. + public Task InitializeAsync(CancellationToken cancellationToken = default) + { + if (_initializationTask == null) + { + // Client was created with non-DI constructor, already initialized + return Task.CompletedTask; + } + + // Lazy ensures this only runs once even if called multiple times + return _initializationTask.Value; + } + + /// + /// Checks if the client is fully initialized. + /// + public bool IsInitialized => + _initializationTask == null || // Non-DI constructor, always ready + (_initializationTask.IsValueCreated && _initializationTask.Value.IsCompletedSuccessfully); + + /// + /// Helper to ensure initialization before using the client. + /// Throws if initialization failed. + /// + private async Task EnsureInitializedAsync() + { + if (_initializationTask != null) + { + await _initializationTask.Value; // Will throw if initialization failed + } + } + /// /// Helper to create REST client for the public constructor. /// From c242abd6c029b753c69b851cb7277195fe765df0 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sun, 23 Nov 2025 22:57:50 +0000 Subject: [PATCH 03/14] feat: Add support for multiple named Weaviate clients Implements IWeaviateClientFactory for managing multiple Weaviate clients with independent configurations. Each client can have different hosts, ports, credentials, timeouts, and other settings. Key Features: - Named client pattern similar to IHttpClientFactory - Each client has independent configuration - Lazy initialization and caching - Thread-safe client creation - Automatic disposal of all clients New Components: - IWeaviateClientFactory: Interface for client factory - WeaviateClientFactory: Implementation with lazy creation and caching - AddWeaviateClient(): Register named clients - AddWeaviateCloudClient(): Helper for cloud clients Public API: - IWeaviateClientFactory.GetClient(name): Get client synchronously - IWeaviateClientFactory.GetClientAsync(name): Get client asynchronously Extension Methods: - AddWeaviateClientFactory(): Register the factory - AddWeaviateClient(name, configureOptions): Register named client - AddWeaviateClient(name, hostname, ports...): Helper for local clients - AddWeaviateCloudClient(name, endpoint, apiKey): Helper for cloud clients Use Cases: - Multi-environment (prod, staging, local) - Multi-region deployments - Multi-tenant architectures - Different databases/clusters - Analytics vs operational databases Documentation: - MULTIPLE_CLIENTS.md: Comprehensive guide with patterns - MultipleClientsExample.cs: Complete working examples - DifferentConfigsExample.cs: Shows independent configurations Example Usage: ```csharp // Register multiple clients services.AddWeaviateClient("prod", options => { ... }); services.AddWeaviateClient("staging", options => { ... }); services.AddWeaviateClient("local", "localhost", 8080, 50051); // Use in service public class MyService { private readonly IWeaviateClientFactory _factory; public async Task ProcessAsync() { var prod = await _factory.GetClientAsync("prod"); var staging = await _factory.GetClientAsync("staging"); // Each has completely different config } } ``` Breaking Changes: None - Single client pattern (AddWeaviate) still works - Factory is opt-in via AddWeaviateClient --- MULTIPLE_CLIENTS.md | 494 ++++++++++++++++++ src/Example/DifferentConfigsExample.cs | 203 +++++++ src/Example/MultipleClientsExample.cs | 203 +++++++ .../IWeaviateClientFactory.cs | 23 + .../WeaviateClientFactory.cs | 89 ++++ .../WeaviateServiceCollectionExtensions.cs | 95 ++++ 6 files changed, 1107 insertions(+) create mode 100644 MULTIPLE_CLIENTS.md create mode 100644 src/Example/DifferentConfigsExample.cs create mode 100644 src/Example/MultipleClientsExample.cs create mode 100644 src/Weaviate.Client/DependencyInjection/IWeaviateClientFactory.cs create mode 100644 src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs diff --git a/MULTIPLE_CLIENTS.md b/MULTIPLE_CLIENTS.md new file mode 100644 index 00000000..4b79ed67 --- /dev/null +++ b/MULTIPLE_CLIENTS.md @@ -0,0 +1,494 @@ +# Multiple Weaviate Clients + +When you need to connect to multiple Weaviate instances (e.g., production, staging, local dev, or different databases), you can use the **named client pattern** via `IWeaviateClientFactory`. + +## Quick Start + +### Register Multiple Clients + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +// Register multiple named clients +builder.Services.AddWeaviateClient("production", options => +{ + options.RestEndpoint = "prod.weaviate.cloud"; + options.GrpcEndpoint = "grpc-prod.weaviate.cloud"; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("prod-key"); +}); + +builder.Services.AddWeaviateClient("staging", options => +{ + options.RestEndpoint = "staging.weaviate.cloud"; + options.GrpcEndpoint = "grpc-staging.weaviate.cloud"; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("staging-key"); +}); + +builder.Services.AddWeaviateClient("local", "localhost", 8080, 50051); + +// Or use cloud helper +builder.Services.AddWeaviateCloudClient("analytics", "analytics.weaviate.cloud", "api-key"); + +var app = builder.Build(); +``` + +### Use Multiple Clients in Your Service + +```csharp +public class DataSyncService +{ + private readonly IWeaviateClientFactory _clientFactory; + + public DataSyncService(IWeaviateClientFactory clientFactory) + { + _clientFactory = clientFactory; + } + + public async Task SyncProductionToStagingAsync() + { + // Get clients by name + var prodClient = await _clientFactory.GetClientAsync("production"); + var stagingClient = await _clientFactory.GetClientAsync("staging"); + + // Fetch from production + var prodCollection = prodClient.Collections.Use("Product"); + var products = await prodCollection.Query.FetchObjects(limit: 1000); + + // Insert into staging + var stagingCollection = stagingClient.Collections.Use("Product"); + foreach (var product in products.Objects) + { + await stagingCollection.Data.Insert(product.As()!); + } + } +} +``` + +--- + +## Registration Patterns + +### Pattern 1: Inline Configuration + +```csharp +builder.Services.AddWeaviateClient("prod", options => +{ + options.RestEndpoint = "prod.weaviate.cloud"; + options.Credentials = Auth.ApiKey("key"); + options.QueryTimeout = TimeSpan.FromSeconds(30); +}); + +builder.Services.AddWeaviateClient("staging", options => +{ + options.RestEndpoint = "staging.weaviate.cloud"; + options.Credentials = Auth.ApiKey("key"); +}); +``` + +### Pattern 2: Using Helper Methods + +```csharp +// For local instances +builder.Services.AddWeaviateClient( + name: "local", + hostname: "localhost", + restPort: 8080, + grpcPort: 50051 +); + +// For cloud instances +builder.Services.AddWeaviateCloudClient( + name: "prod", + clusterEndpoint: "prod.weaviate.cloud", + apiKey: "prod-key" +); +``` + +### Pattern 3: From Configuration + +```json +// appsettings.json +{ + "Weaviate": { + "Clients": { + "Production": { + "RestEndpoint": "prod.weaviate.cloud", + "GrpcEndpoint": "grpc-prod.weaviate.cloud", + "UseSsl": true, + "RestPort": 443, + "GrpcPort": 443 + }, + "Staging": { + "RestEndpoint": "staging.weaviate.cloud", + "GrpcEndpoint": "grpc-staging.weaviate.cloud", + "UseSsl": true, + "RestPort": 443, + "GrpcPort": 443 + }, + "Local": { + "RestEndpoint": "localhost", + "GrpcEndpoint": "localhost", + "RestPort": 8080, + "GrpcPort": 50051 + } + } + } +} +``` + +```csharp +// Program.cs +var clientsConfig = builder.Configuration.GetSection("Weaviate:Clients"); + +foreach (var client in new[] { "Production", "Staging", "Local" }) +{ + builder.Services.AddWeaviateClient( + client.ToLower(), + clientsConfig.GetSection(client).Get()!); +} +``` + +--- + +## Usage Patterns + +### Pattern 1: Inject Factory + +Most flexible - get clients on demand: + +```csharp +public class MyService +{ + private readonly IWeaviateClientFactory _factory; + + public MyService(IWeaviateClientFactory factory) + { + _factory = factory; + } + + public async Task ProcessAsync(string environment) + { + // Get client based on runtime logic + var client = await _factory.GetClientAsync(environment); + + var collection = client.Collections.Use("Data"); + var results = await collection.Query.FetchObjects(); + + // Process... + } +} +``` + +### Pattern 2: Get Specific Clients + +Clearer intent when you always use specific clients: + +```csharp +public class MultiEnvironmentService +{ + private readonly WeaviateClient _prodClient; + private readonly WeaviateClient _stagingClient; + + public MultiEnvironmentService(IWeaviateClientFactory factory) + { + // Get clients once in constructor + // Note: Using synchronous GetClient() - will block during DI resolution + _prodClient = factory.GetClient("production"); + _stagingClient = factory.GetClient("staging"); + } + + public async Task SyncAsync() + { + // Use pre-fetched clients + var prodData = await _prodClient.Collections.Use("Data") + .Query.FetchObjects(); + + var stagingCollection = _stagingClient.Collections.Use("Data"); + // ... + } +} +``` + +### Pattern 3: Lazy Client Resolution + +Best for async initialization: + +```csharp +public class LazyClientService +{ + private readonly IWeaviateClientFactory _factory; + private WeaviateClient? _prodClient; + private WeaviateClient? _stagingClient; + + public LazyClientService(IWeaviateClientFactory factory) + { + _factory = factory; + } + + private async Task GetProdClientAsync() + { + return _prodClient ??= await _factory.GetClientAsync("production"); + } + + private async Task GetStagingClientAsync() + { + return _stagingClient ??= await _factory.GetClientAsync("staging"); + } + + public async Task ProcessAsync() + { + var prod = await GetProdClientAsync(); + var staging = await GetStagingClientAsync(); + + // Use clients... + } +} +``` + +--- + +## Common Scenarios + +### Scenario 1: Multi-Region Data Sync + +```csharp +public class MultiRegionSyncService +{ + private readonly IWeaviateClientFactory _factory; + + public MultiRegionSyncService(IWeaviateClientFactory factory) + { + _factory = factory; + } + + public async Task SyncAcrossRegionsAsync() + { + var usClient = await _factory.GetClientAsync("us-east"); + var euClient = await _factory.GetClientAsync("eu-west"); + var apacClient = await _factory.GetClientAsync("apac"); + + // Fetch from primary region + var usCollection = usClient.Collections.Use("User"); + var users = await usCollection.Query.FetchObjects(limit: 10000); + + // Replicate to other regions + var euCollection = euClient.Collections.Use("User"); + var apacCollection = apacClient.Collections.Use("User"); + + foreach (var user in users.Objects) + { + var userData = user.As()!; + await euCollection.Data.Insert(userData); + await apacCollection.Data.Insert(userData); + } + } +} +``` + +### Scenario 2: Environment-Based Processing + +```csharp +public class EnvironmentAwareService +{ + private readonly IWeaviateClientFactory _factory; + private readonly IHostEnvironment _environment; + + public EnvironmentAwareService( + IWeaviateClientFactory factory, + IHostEnvironment environment) + { + _factory = factory; + _environment = environment; + } + + public async Task GetCurrentEnvironmentClientAsync() + { + var clientName = _environment.IsDevelopment() ? "local" : + _environment.IsStaging() ? "staging" : + "production"; + + return await _factory.GetClientAsync(clientName); + } + + public async Task ProcessAsync() + { + var client = await GetCurrentEnvironmentClientAsync(); + var collection = client.Collections.Use("Data"); + + // Process with appropriate environment client... + } +} +``` + +### Scenario 3: Multi-Tenant Architecture + +```csharp +public class MultiTenantService +{ + private readonly IWeaviateClientFactory _factory; + + public MultiTenantService(IWeaviateClientFactory factory) + { + _factory = factory; + } + + // Register clients per tenant + public static void ConfigureTenants(IServiceCollection services) + { + services.AddWeaviateClient("tenant-acme", options => + { + options.RestEndpoint = "acme.weaviate.cloud"; + options.Credentials = Auth.ApiKey("acme-key"); + }); + + services.AddWeaviateClient("tenant-globex", options => + { + options.RestEndpoint = "globex.weaviate.cloud"; + options.Credentials = Auth.ApiKey("globex-key"); + }); + } + + public async Task> GetTenantProductsAsync(string tenantId) + { + var clientName = $"tenant-{tenantId}"; + var client = await _factory.GetClientAsync(clientName); + + var collection = client.Collections.Use("Product"); + var results = await collection.Query.FetchObjects(limit: 100); + + return results.Objects.Select(o => o.As()!).ToList(); + } +} +``` + +### Scenario 4: Testing vs Production + +```csharp +public class TestableService +{ + private readonly IWeaviateClientFactory _factory; + + public TestableService(IWeaviateClientFactory factory) + { + _factory = factory; + } + + public async Task ProcessDataAsync(bool useTestDatabase = false) + { + var clientName = useTestDatabase ? "test" : "production"; + var client = await _factory.GetClientAsync(clientName); + + var collection = client.Collections.Use("Data"); + var results = await collection.Query.FetchObjects(); + + // Process... + } +} + +// In tests +public class TestableServiceTests +{ + [Fact] + public async Task TestWithTestDatabase() + { + var services = new ServiceCollection(); + + // Register test database + services.AddWeaviateClient("test", "localhost", 8080, 50051); + + var provider = services.BuildServiceProvider(); + var service = new TestableService( + provider.GetRequiredService()); + + await service.ProcessDataAsync(useTestDatabase: true); + } +} +``` + +--- + +## Client Lifecycle + +- **Creation**: Clients are created lazily on first access via the factory +- **Caching**: Once created, clients are cached for the lifetime of the factory +- **Initialization**: Each client initializes asynchronously when created +- **Disposal**: All clients are disposed when the factory is disposed + +--- + +## Mixing Single and Multiple Client Patterns + +You can use both patterns in the same application: + +```csharp +// Register a default single client +builder.Services.AddWeaviate(options => +{ + options.RestEndpoint = "localhost"; +}); + +// Also register named clients +builder.Services.AddWeaviateClient("analytics", "analytics.weaviate.cloud"); +builder.Services.AddWeaviateClient("backup", "backup.weaviate.cloud"); + +// Usage +public class MixedService +{ + private readonly WeaviateClient _defaultClient; + private readonly IWeaviateClientFactory _factory; + + public MixedService( + WeaviateClient defaultClient, + IWeaviateClientFactory factory) + { + _defaultClient = defaultClient; // Default client + _factory = factory; // For named clients + } + + public async Task ProcessAsync() + { + // Use default client for main operations + var mainData = await _defaultClient.Collections.Use("Data") + .Query.FetchObjects(); + + // Use analytics client for metrics + var analyticsClient = await _factory.GetClientAsync("analytics"); + var metrics = await analyticsClient.Collections.Use("Metric") + .Query.FetchObjects(); + } +} +``` + +--- + +## Best Practices + +1. **Use descriptive names**: `"production"`, `"staging"`, `"analytics"` instead of `"client1"`, `"client2"` + +2. **Get clients asynchronously**: Use `GetClientAsync()` when possible to avoid blocking + +3. **Cache clients in services**: If you always use specific clients, get them once + +4. **Configuration over code**: Store connection details in `appsettings.json` + +5. **Environment-based selection**: Use `IHostEnvironment` to select appropriate clients + +6. **Don't create clients manually**: Always use the factory for proper lifecycle management + +--- + +## Summary + +✅ **Multiple clients** - Connect to multiple Weaviate instances +✅ **Named clients** - Identify clients by logical names +✅ **Lazy initialization** - Clients created on first use +✅ **Automatic caching** - Clients reused across the application +✅ **Thread-safe** - Factory handles concurrent access +✅ **Proper disposal** - All clients cleaned up with factory +✅ **Flexible usage** - Inject factory or specific clients +✅ **Configuration-friendly** - Works with `appsettings.json` + +For single-client scenarios, see [DEPENDENCY_INJECTION.md](DEPENDENCY_INJECTION.md). diff --git a/src/Example/DifferentConfigsExample.cs b/src/Example/DifferentConfigsExample.cs new file mode 100644 index 00000000..c3865628 --- /dev/null +++ b/src/Example/DifferentConfigsExample.cs @@ -0,0 +1,203 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Weaviate.Client; +using Weaviate.Client.DependencyInjection; + +namespace Example; + +/// +/// Example showing how each named client can have completely different configuration. +/// Each client has its own: host, port, credentials, timeouts, SSL settings, etc. +/// +public class DifferentConfigsExample +{ + public static async Task Main(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Client 1: Production cloud with SSL and API key + services.AddWeaviateClient("production", options => + { + options.RestEndpoint = "prod.weaviate.cloud"; + options.GrpcEndpoint = "grpc-prod.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("prod-api-key-here"); + options.DefaultTimeout = TimeSpan.FromSeconds(60); + options.QueryTimeout = TimeSpan.FromSeconds(30); + options.Headers = new Dictionary + { + ["X-Environment"] = "production" + }; + }); + + // Client 2: Local development, no SSL, no auth, longer timeouts + services.AddWeaviateClient("local", options => + { + options.RestEndpoint = "localhost"; + options.GrpcEndpoint = "localhost"; + options.RestPort = 8080; + options.GrpcPort = 50051; + options.UseSsl = false; + options.Credentials = null; // No auth for local + options.DefaultTimeout = TimeSpan.FromSeconds(120); // Longer for debugging + options.QueryTimeout = TimeSpan.FromSeconds(300); // Very long for local testing + }); + + // Client 3: Staging with OAuth credentials + services.AddWeaviateClient("staging", options => + { + options.RestEndpoint = "staging.weaviate.cloud"; + options.GrpcEndpoint = "grpc-staging.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ClientCredentials( + "staging-client-secret", + "weaviate.read", + "weaviate.write" + ); + options.DefaultTimeout = TimeSpan.FromSeconds(45); + }); + + // Client 4: Analytics cluster with custom ports and retry policy + services.AddWeaviateClient("analytics", options => + { + options.RestEndpoint = "analytics.internal.company.com"; + options.GrpcEndpoint = "analytics.internal.company.com"; + options.RestPort = 9090; // Custom port + options.GrpcPort = 9091; // Custom port + options.UseSsl = true; + options.Credentials = Auth.ApiKey("analytics-key"); + options.QueryTimeout = TimeSpan.FromSeconds(120); // Slow analytics queries + options.RetryPolicy = new RetryPolicy + { + MaxRetries = 5, // More retries for unreliable network + RetryDelay = TimeSpan.FromSeconds(2) + }; + }); + + // Client 5: Legacy system with password auth + services.AddWeaviateClient("legacy", options => + { + options.RestEndpoint = "legacy.oldserver.com"; + options.GrpcEndpoint = "legacy.oldserver.com"; + options.RestPort = 8081; + options.GrpcPort = 50052; + options.UseSsl = false; // Old server doesn't support SSL + options.Credentials = Auth.ClientPassword( + "legacy-username", + "legacy-password" + ); + options.InitTimeout = TimeSpan.FromSeconds(10); // Slow to start + }); + + services.AddSingleton(); + }) + .Build(); + + await host.StartAsync(); + + var service = host.Services.GetRequiredService(); + await service.ShowDifferentConfigsAsync(); + + await host.StopAsync(); + } +} + +public class MultiConfigService +{ + private readonly IWeaviateClientFactory _factory; + + public MultiConfigService(IWeaviateClientFactory factory) + { + _factory = factory; + } + + public async Task ShowDifferentConfigsAsync() + { + Console.WriteLine("=== Different Client Configurations ===\n"); + + // Get all clients - each with different configuration + var prodClient = await _factory.GetClientAsync("production"); + var localClient = await _factory.GetClientAsync("local"); + var stagingClient = await _factory.GetClientAsync("staging"); + var analyticsClient = await _factory.GetClientAsync("analytics"); + var legacyClient = await _factory.GetClientAsync("legacy"); + + // Show each client's configuration + Console.WriteLine($"Production:"); + Console.WriteLine($" - Endpoint: {prodClient.Configuration.RestAddress}:{prodClient.Configuration.RestPort}"); + Console.WriteLine($" - SSL: {prodClient.Configuration.UseSsl}"); + Console.WriteLine($" - Version: {prodClient.WeaviateVersion}"); + Console.WriteLine($" - Query Timeout: {prodClient.QueryTimeout}"); + Console.WriteLine(); + + Console.WriteLine($"Local:"); + Console.WriteLine($" - Endpoint: {localClient.Configuration.RestAddress}:{localClient.Configuration.RestPort}"); + Console.WriteLine($" - SSL: {localClient.Configuration.UseSsl}"); + Console.WriteLine($" - Version: {localClient.WeaviateVersion}"); + Console.WriteLine($" - Query Timeout: {localClient.QueryTimeout}"); + Console.WriteLine(); + + Console.WriteLine($"Staging:"); + Console.WriteLine($" - Endpoint: {stagingClient.Configuration.RestAddress}:{stagingClient.Configuration.RestPort}"); + Console.WriteLine($" - SSL: {stagingClient.Configuration.UseSsl}"); + Console.WriteLine($" - Version: {stagingClient.WeaviateVersion}"); + Console.WriteLine(); + + Console.WriteLine($"Analytics:"); + Console.WriteLine($" - Endpoint: {analyticsClient.Configuration.RestAddress}:{analyticsClient.Configuration.RestPort}"); + Console.WriteLine($" - Custom Ports: REST={analyticsClient.Configuration.RestPort}, gRPC={analyticsClient.Configuration.GrpcPort}"); + Console.WriteLine($" - Version: {analyticsClient.WeaviateVersion}"); + Console.WriteLine(); + + Console.WriteLine($"Legacy:"); + Console.WriteLine($" - Endpoint: {legacyClient.Configuration.RestAddress}:{legacyClient.Configuration.RestPort}"); + Console.WriteLine($" - SSL: {legacyClient.Configuration.UseSsl}"); + Console.WriteLine($" - Version: {legacyClient.WeaviateVersion}"); + + // Now use them with completely different configurations + await UseProductionClient(prodClient); + await UseLocalClient(localClient); + await UseAnalyticsClient(analyticsClient); + } + + private async Task UseProductionClient(WeaviateClient client) + { + // Production has strict timeouts and requires auth + var collection = client.Collections.Use("Product"); + var results = await collection.Query.FetchObjects(limit: 10); + // This will use 30s query timeout configured above + } + + private async Task UseLocalClient(WeaviateClient client) + { + // Local has no auth and longer timeouts for debugging + var collection = client.Collections.Use("Product"); + var results = await collection.Query.FetchObjects(limit: 100); + // This will use 300s query timeout - perfect for debugging + } + + private async Task UseAnalyticsClient(WeaviateClient client) + { + // Analytics has custom ports and longer timeouts for slow queries + var collection = client.Collections.Use("Metric"); + var results = await collection.Query.FetchObjects(limit: 10000); + // This will use 120s query timeout and retry 5 times if it fails + } +} + +public class Product +{ + public string? Name { get; set; } + public decimal Price { get; set; } +} + +public class Metric +{ + public string? Name { get; set; } + public double Value { get; set; } +} diff --git a/src/Example/MultipleClientsExample.cs b/src/Example/MultipleClientsExample.cs new file mode 100644 index 00000000..4f988798 --- /dev/null +++ b/src/Example/MultipleClientsExample.cs @@ -0,0 +1,203 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Weaviate.Client; +using Weaviate.Client.DependencyInjection; + +namespace Example; + +/// +/// Example demonstrating how to use multiple Weaviate clients via dependency injection. +/// +public class MultipleClientsExample +{ + public static async Task Main(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Register multiple named Weaviate clients + services.AddWeaviateClient("production", options => + { + options.RestEndpoint = "prod.weaviate.cloud"; + options.GrpcEndpoint = "grpc-prod.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("prod-api-key"); + }); + + services.AddWeaviateClient("staging", options => + { + options.RestEndpoint = "staging.weaviate.cloud"; + options.GrpcEndpoint = "grpc-staging.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("staging-api-key"); + }); + + services.AddWeaviateClient("local", "localhost", 8080, 50051); + + // Or use helper methods + services.AddWeaviateCloudClient("analytics", "analytics.weaviate.cloud", "analytics-key"); + + // Register services that use multiple clients + services.AddSingleton(); + }) + .Build(); + + await host.StartAsync(); + + var service = host.Services.GetRequiredService(); + await service.DemonstrateMultipleClientsAsync(); + + await host.StopAsync(); + } +} + +/// +/// Service that uses multiple Weaviate clients simultaneously. +/// +public class MultiDatabaseService +{ + private readonly IWeaviateClientFactory _clientFactory; + private readonly ILogger _logger; + + public MultiDatabaseService( + IWeaviateClientFactory clientFactory, + ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + public async Task DemonstrateMultipleClientsAsync() + { + _logger.LogInformation("=== Multiple Weaviate Clients Example ===\n"); + + // Get different clients by name + var prodClient = await _clientFactory.GetClientAsync("production"); + var stagingClient = await _clientFactory.GetClientAsync("staging"); + var localClient = await _clientFactory.GetClientAsync("local"); + + _logger.LogInformation("Production client version: {Version}", prodClient.WeaviateVersion); + _logger.LogInformation("Staging client version: {Version}", stagingClient.WeaviateVersion); + _logger.LogInformation("Local client version: {Version}", localClient.WeaviateVersion); + + // Use different clients for different purposes + await SyncDataBetweenEnvironmentsAsync(prodClient, stagingClient); + await TestLocallyAsync(localClient); + } + + private async Task SyncDataBetweenEnvironmentsAsync( + WeaviateClient prodClient, + WeaviateClient stagingClient) + { + _logger.LogInformation("\nSyncing data from production to staging..."); + + var prodCollection = prodClient.Collections.Use("Cat"); + var stagingCollection = stagingClient.Collections.Use("Cat"); + + // Fetch from production + var prodResults = await prodCollection.Query.FetchObjects(limit: 100); + _logger.LogInformation("Found {Count} cats in production", prodResults.Objects.Count()); + + // Insert into staging + var cats = prodResults.Objects.Select(o => o.As()!); + foreach (var cat in cats) + { + await stagingCollection.Data.Insert(cat); + } + + _logger.LogInformation("Synced to staging environment"); + } + + private async Task TestLocallyAsync(WeaviateClient localClient) + { + _logger.LogInformation("\nTesting locally..."); + + var localCollection = localClient.Collections.Use("Cat"); + + // Test queries locally before deploying to production + var results = await localCollection.Query.FetchObjects(limit: 10); + _logger.LogInformation("Local test completed: {Count} results", results.Objects.Count()); + } +} + +/// +/// Alternative pattern: Inject factory and get clients on demand. +/// +public class OnDemandClientService +{ + private readonly IWeaviateClientFactory _clientFactory; + + public OnDemandClientService(IWeaviateClientFactory clientFactory) + { + _clientFactory = clientFactory; + } + + public async Task ProcessDataFromEnvironmentAsync(string environment) + { + // Get the appropriate client based on runtime logic + var client = await _clientFactory.GetClientAsync(environment); + + var collection = client.Collections.Use("Cat"); + var results = await collection.Query.FetchObjects(limit: 100); + + // Process results... + } +} + +/// +/// Example with configuration from appsettings.json +/// +public class ConfigurationBasedMultiClientExample +{ + public static async Task RunAsync() + { + /* + * appsettings.json: + * { + * "Weaviate": { + * "Production": { + * "RestEndpoint": "prod.weaviate.cloud", + * "ApiKey": "prod-key" + * }, + * "Staging": { + * "RestEndpoint": "staging.weaviate.cloud", + * "ApiKey": "staging-key" + * } + * } + * } + */ + + var host = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + // Register clients from configuration + services.AddWeaviateClient( + "production", + context.Configuration.GetSection("Weaviate:Production") + .Get()! + .RestEndpoint); + + services.AddWeaviateClient( + "staging", + context.Configuration.GetSection("Weaviate:Staging") + .Get()! + .RestEndpoint); + }) + .Build(); + + await host.StartAsync(); + + var factory = host.Services.GetRequiredService(); + var prodClient = await factory.GetClientAsync("production"); + var stagingClient = await factory.GetClientAsync("staging"); + + // Use clients... + + await host.StopAsync(); + } +} diff --git a/src/Weaviate.Client/DependencyInjection/IWeaviateClientFactory.cs b/src/Weaviate.Client/DependencyInjection/IWeaviateClientFactory.cs new file mode 100644 index 00000000..acbeaa1e --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/IWeaviateClientFactory.cs @@ -0,0 +1,23 @@ +namespace Weaviate.Client.DependencyInjection; + +/// +/// Factory for creating and managing multiple named Weaviate clients. +/// +public interface IWeaviateClientFactory +{ + /// + /// Gets or creates a Weaviate client with the specified name. + /// + /// The logical name of the client to create. + /// A WeaviateClient instance. + WeaviateClient GetClient(string name); + + /// + /// Gets or creates a Weaviate client asynchronously with the specified name. + /// Ensures the client is fully initialized before returning. + /// + /// The logical name of the client to create. + /// Cancellation token. + /// A fully initialized WeaviateClient instance. + Task GetClientAsync(string name, CancellationToken cancellationToken = default); +} diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs b/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs new file mode 100644 index 00000000..3157ed16 --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs @@ -0,0 +1,89 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Weaviate.Client.DependencyInjection; + +/// +/// Factory for creating and managing multiple named Weaviate clients. +/// Clients are created lazily and cached for the lifetime of the factory. +/// +internal class WeaviateClientFactory : IWeaviateClientFactory, IDisposable +{ + private readonly IOptionsMonitor _optionsMonitor; + private readonly ILoggerFactory _loggerFactory; + private readonly ConcurrentDictionary>> _clients = new(); + private bool _disposed; + + public WeaviateClientFactory( + IOptionsMonitor optionsMonitor, + ILoggerFactory loggerFactory) + { + _optionsMonitor = optionsMonitor; + _loggerFactory = loggerFactory; + } + + /// + /// Gets or creates a client synchronously. + /// If the client hasn't been initialized yet, this will block until initialization completes. + /// Consider using GetClientAsync() for better async behavior. + /// + public WeaviateClient GetClient(string name) + { + if (_disposed) + throw new ObjectDisposedException(nameof(WeaviateClientFactory)); + + var lazyClient = _clients.GetOrAdd(name, n => new Lazy>( + () => CreateClientAsync(n))); + + // This will block if the client is still initializing + // For non-blocking behavior, use GetClientAsync() + return lazyClient.Value.GetAwaiter().GetResult(); + } + + /// + /// Gets or creates a client asynchronously. + /// Ensures the client is fully initialized before returning. + /// + public async Task GetClientAsync(string name, CancellationToken cancellationToken = default) + { + if (_disposed) + throw new ObjectDisposedException(nameof(WeaviateClientFactory)); + + var lazyClient = _clients.GetOrAdd(name, n => new Lazy>( + () => CreateClientAsync(n))); + + return await lazyClient.Value; + } + + private async Task CreateClientAsync(string name) + { + var options = _optionsMonitor.Get(name); + var logger = _loggerFactory.CreateLogger(); + + var clientOptions = Options.Create(options); + var client = new WeaviateClient(clientOptions, logger); + + // Initialize the client + await client.InitializeAsync(); + + return client; + } + + public void Dispose() + { + if (_disposed) + return; + + foreach (var lazyClient in _clients.Values) + { + if (lazyClient.IsValueCreated && lazyClient.Value.IsCompletedSuccessfully) + { + lazyClient.Value.Result.Dispose(); + } + } + + _clients.Clear(); + _disposed = true; + } +} diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs index a70e3c05..aba41d32 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs @@ -9,8 +9,45 @@ namespace Weaviate.Client.DependencyInjection; /// public static class WeaviateServiceCollectionExtensions { + /// + /// Adds Weaviate client factory for managing multiple named clients. + /// Use this when you need to connect to multiple Weaviate instances. + /// + /// The service collection. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateClientFactory(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + + /// + /// Adds a named Weaviate client to the factory. + /// + /// The service collection. + /// The logical name of the client. + /// Action to configure Weaviate options for this client. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateClient( + this IServiceCollection services, + string name, + Action configureOptions) + { + // Ensure factory is registered + if (!services.Any(x => x.ServiceType == typeof(IWeaviateClientFactory))) + { + services.AddWeaviateClientFactory(); + } + + // Configure options for this named client + services.Configure(name, configureOptions); + + return services; + } + /// /// Adds Weaviate client services to the dependency injection container. + /// This registers a single default client. /// /// The service collection. /// Action to configure Weaviate options. @@ -114,4 +151,62 @@ public static IServiceCollection AddWeaviateCloud( return services; } + + // Named client helper methods + + /// + /// Adds a named Weaviate client configured for a local instance. + /// + /// The service collection. + /// The logical name of the client. + /// Hostname for local Weaviate instance. Default is "localhost". + /// REST port. Default is 8080. + /// gRPC port. Default is 50051. + /// Whether to use SSL/TLS. Default is false. + /// Authentication credentials. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateClient( + this IServiceCollection services, + string name, + string hostname = "localhost", + ushort restPort = 8080, + ushort grpcPort = 50051, + bool useSsl = false, + ICredentials? credentials = null) + { + return services.AddWeaviateClient(name, options => + { + options.RestEndpoint = hostname; + options.GrpcEndpoint = hostname; + options.RestPort = restPort; + options.GrpcPort = grpcPort; + options.UseSsl = useSsl; + options.Credentials = credentials; + }); + } + + /// + /// Adds a named Weaviate client configured for Weaviate Cloud. + /// + /// The service collection. + /// The logical name of the client. + /// The Weaviate Cloud cluster endpoint (e.g., "my-cluster.weaviate.cloud"). + /// API key for authentication. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateCloudClient( + this IServiceCollection services, + string name, + string clusterEndpoint, + string? apiKey = null) + { + return services.AddWeaviateClient(name, options => + { + options.RestEndpoint = clusterEndpoint; + options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); + }); + } } From a14e069879e4d7429da71bdbcf2da5f12938d90f Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sun, 23 Nov 2025 23:05:27 +0000 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20Enhance=20AddWeaviateLocal=20and?= =?UTF-8?q?=20AddWeaviateCloud=20with=20full=20parameter=20support=20Adds?= =?UTF-8?q?=20headers=20and=20timeout=20parameters=20to=20match=20the=20Co?= =?UTF-8?q?nnect.Local()=20and=20Connect.Cloud()=20helper=20API=20signatur?= =?UTF-8?q?es,=20providing=20consistency=20between=20DI=20registration=20a?= =?UTF-8?q?nd=20direct=20client=20creation=20patterns.=20Changes:=20-=20Ad?= =?UTF-8?q?dWeaviateLocal():=20Added=20headers,=20defaultTimeout,=20initTi?= =?UTF-8?q?meout,=20dataTimeout,=20queryTimeout=20-=20AddWeaviateCloud():?= =?UTF-8?q?=20Added=20headers,=20defaultTimeout,=20initTimeout,=20dataTime?= =?UTF-8?q?out,=20queryTimeout=20Now=20these=20methods=20have=20the=20same?= =?UTF-8?q?=20configuration=20options=20as=20Connect.Local/Cloud:=20```csh?= =?UTF-8?q?arp=20//=20Single=20client=20DI=20registration=20-=20now=20with?= =?UTF-8?q?=20full=20options=20builder.Services.AddWeaviateLocal(=20=20=20?= =?UTF-8?q?=20=20hostname:=20"localhost",=20=20=20=20=20headers:=20new=20D?= =?UTF-8?q?ictionary=20{=20["X-Custom"]=20=3D=20"value"?= =?UTF-8?q?=20},=20=20=20=20=20queryTimeout:=20TimeSpan.FromSeconds(120),?= =?UTF-8?q?=20=20=20=20=20eagerInitialization:=20true=20);=20builder.Servi?= =?UTF-8?q?ces.AddWeaviateCloud(=20=20=20=20=20clusterEndpoint:=20"my-clus?= =?UTF-8?q?ter.weaviate.cloud",=20=20=20=20=20apiKey:=20"key",=20=20=20=20?= =?UTF-8?q?=20headers:=20customHeaders,=20=20=20=20=20defaultTimeout:=20Ti?= =?UTF-8?q?meSpan.FromMinutes(1)=20);=20//=20Matches=20the=20pattern=20of:?= =?UTF-8?q?=20var=20client=20=3D=20await=20Connect.Local(=20=20=20=20=20ho?= =?UTF-8?q?stname:=20"localhost",=20=20=20=20=20headers:=20new=20Dictionar?= =?UTF-8?q?y=20{=20["X-Custom"]=20=3D=20"value"=20},=20?= =?UTF-8?q?=20=20=20=20queryTimeout:=20TimeSpan.FromSeconds(120)=20);=20``?= =?UTF-8?q?`=20Benefits:=20=E2=9C=85=20API=20consistency=20between=20DI=20?= =?UTF-8?q?and=20Connect=20helpers=20=E2=9C=85=20Full=20control=20over=20t?= =?UTF-8?q?imeouts=20in=20DI=20scenarios=20=E2=9C=85=20Support=20for=20cus?= =?UTF-8?q?tom=20headers=20=E2=9C=85=20No=20need=20to=20use=20Configure=20for=20simple=20cases=20Breaking=20Changes:=20N?= =?UTF-8?q?one=20-=20All=20new=20parameters=20are=20optional=20with=20sens?= =?UTF-8?q?ible=20defaults=20-=20Existing=20code=20continues=20to=20work?= =?UTF-8?q?=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WeaviateServiceCollectionExtensions.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs index aba41d32..ea48cd93 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs @@ -93,7 +93,8 @@ public static IServiceCollection AddWeaviate( } /// - /// Adds Weaviate client services with connection helpers. + /// Adds Weaviate client services for a local Weaviate instance. + /// Similar to Connect.Local() but for dependency injection. /// /// The service collection. /// Hostname for local Weaviate instance. Default is "localhost". @@ -101,6 +102,11 @@ public static IServiceCollection AddWeaviate( /// gRPC port. Default is 50051. /// Whether to use SSL/TLS. Default is false. /// Authentication credentials. + /// Additional HTTP headers to include in requests. + /// Default timeout for all operations. + /// Timeout for initialization operations. + /// Timeout for data operations. + /// Timeout for query operations. /// Whether to initialize the client eagerly on application startup. Default is true. /// The service collection for method chaining. public static IServiceCollection AddWeaviateLocal( @@ -110,6 +116,11 @@ public static IServiceCollection AddWeaviateLocal( ushort grpcPort = 50051, bool useSsl = false, ICredentials? credentials = null, + Dictionary? headers = null, + TimeSpan? defaultTimeout = null, + TimeSpan? initTimeout = null, + TimeSpan? dataTimeout = null, + TimeSpan? queryTimeout = null, bool eagerInitialization = true) { services.AddWeaviate(options => @@ -120,6 +131,11 @@ public static IServiceCollection AddWeaviateLocal( options.GrpcPort = grpcPort; options.UseSsl = useSsl; options.Credentials = credentials; + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; }, eagerInitialization); return services; @@ -127,16 +143,27 @@ public static IServiceCollection AddWeaviateLocal( /// /// Adds Weaviate client services configured for Weaviate Cloud. + /// Similar to Connect.Cloud() but for dependency injection. /// /// The service collection. /// The Weaviate Cloud cluster endpoint (e.g., "my-cluster.weaviate.cloud"). /// API key for authentication. + /// Additional HTTP headers to include in requests. + /// Default timeout for all operations. + /// Timeout for initialization operations. + /// Timeout for data operations. + /// Timeout for query operations. /// Whether to initialize the client eagerly on application startup. Default is true. /// The service collection for method chaining. public static IServiceCollection AddWeaviateCloud( this IServiceCollection services, string clusterEndpoint, string? apiKey = null, + Dictionary? headers = null, + TimeSpan? defaultTimeout = null, + TimeSpan? initTimeout = null, + TimeSpan? dataTimeout = null, + TimeSpan? queryTimeout = null, bool eagerInitialization = true) { services.AddWeaviate(options => @@ -147,6 +174,11 @@ public static IServiceCollection AddWeaviateCloud( options.GrpcPort = 443; options.UseSsl = true; options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; }, eagerInitialization); return services; From 54b295cfe707bf9613bdd682dae8ed53deed2ef6 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Mon, 24 Nov 2025 10:22:53 +0000 Subject: [PATCH 05/14] feat: Add intuitive named client registration API - Add AddWeaviateLocal(name) with default localhost:8080 configuration - Add AddWeaviateLocal(name, Action) for custom configuration - Add AddWeaviateLocal(name, hostname, ports, ...) with full parameter support - Add equivalent AddWeaviateCloud(name, ...) overloads for cloud clients - Mark old AddWeaviateClient() and AddWeaviateCloudClient() as obsolete This provides a more intuitive API where users can write: services.AddWeaviateLocal("local-rbac"); services.AddWeaviateLocal("local-test", options => { ... }); services.AddWeaviateCloud("prod", "my-cluster.weaviate.cloud", "api-key"); --- .../WeaviateServiceCollectionExtensions.cs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs index ea48cd93..86b93e30 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs @@ -186,6 +186,143 @@ public static IServiceCollection AddWeaviateCloud( // Named client helper methods + /// + /// Adds a named Weaviate client for a local instance with default configuration. + /// Uses localhost:8080 for REST and localhost:50051 for gRPC. + /// + /// The service collection. + /// The logical name of the client. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateLocal( + this IServiceCollection services, + string name) + { + return services.AddWeaviateClient(name, options => + { + options.RestEndpoint = "localhost"; + options.GrpcEndpoint = "localhost"; + options.RestPort = 8080; + options.GrpcPort = 50051; + options.UseSsl = false; + }); + } + + /// + /// Adds a named Weaviate client for a local instance with custom configuration. + /// + /// The service collection. + /// The logical name of the client. + /// Action to configure Weaviate options for this client. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateLocal( + this IServiceCollection services, + string name, + Action configureOptions) + { + return services.AddWeaviateClient(name, configureOptions); + } + + /// + /// Adds a named Weaviate client for a local instance with specific parameters. + /// + /// The service collection. + /// The logical name of the client. + /// Hostname for local Weaviate instance. Default is "localhost". + /// REST port. Default is 8080. + /// gRPC port. Default is 50051. + /// Whether to use SSL/TLS. Default is false. + /// Authentication credentials. + /// Additional HTTP headers to include in requests. + /// Default timeout for all operations. + /// Timeout for initialization operations. + /// Timeout for data operations. + /// Timeout for query operations. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateLocal( + this IServiceCollection services, + string name, + string hostname = "localhost", + ushort restPort = 8080, + ushort grpcPort = 50051, + bool useSsl = false, + ICredentials? credentials = null, + Dictionary? headers = null, + TimeSpan? defaultTimeout = null, + TimeSpan? initTimeout = null, + TimeSpan? dataTimeout = null, + TimeSpan? queryTimeout = null) + { + return services.AddWeaviateClient(name, options => + { + options.RestEndpoint = hostname; + options.GrpcEndpoint = hostname; + options.RestPort = restPort; + options.GrpcPort = grpcPort; + options.UseSsl = useSsl; + options.Credentials = credentials; + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; + }); + } + + /// + /// Adds a named Weaviate client for Weaviate Cloud with custom configuration. + /// + /// The service collection. + /// The logical name of the client. + /// Action to configure Weaviate options for this client. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateCloud( + this IServiceCollection services, + string name, + Action configureOptions) + { + return services.AddWeaviateClient(name, configureOptions); + } + + /// + /// Adds a named Weaviate client for Weaviate Cloud with specific parameters. + /// + /// The service collection. + /// The logical name of the client. + /// The Weaviate Cloud cluster endpoint (e.g., "my-cluster.weaviate.cloud"). + /// API key for authentication. + /// Additional HTTP headers to include in requests. + /// Default timeout for all operations. + /// Timeout for initialization operations. + /// Timeout for data operations. + /// Timeout for query operations. + /// The service collection for method chaining. + public static IServiceCollection AddWeaviateCloud( + this IServiceCollection services, + string name, + string clusterEndpoint, + string? apiKey = null, + Dictionary? headers = null, + TimeSpan? defaultTimeout = null, + TimeSpan? initTimeout = null, + TimeSpan? dataTimeout = null, + TimeSpan? queryTimeout = null) + { + return services.AddWeaviateClient(name, options => + { + options.RestEndpoint = clusterEndpoint; + options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; + }); + } + /// /// Adds a named Weaviate client configured for a local instance. /// @@ -197,6 +334,7 @@ public static IServiceCollection AddWeaviateCloud( /// Whether to use SSL/TLS. Default is false. /// Authentication credentials. /// The service collection for method chaining. + [Obsolete("Use AddWeaviateLocal(name, hostname, ...) instead for better API consistency.")] public static IServiceCollection AddWeaviateClient( this IServiceCollection services, string name, @@ -225,6 +363,7 @@ public static IServiceCollection AddWeaviateClient( /// The Weaviate Cloud cluster endpoint (e.g., "my-cluster.weaviate.cloud"). /// API key for authentication. /// The service collection for method chaining. + [Obsolete("Use AddWeaviateCloud(name, clusterEndpoint, apiKey) instead for better API consistency.")] public static IServiceCollection AddWeaviateCloudClient( this IServiceCollection services, string name, From 7dfd36d59c9efa3c1dfa3598437b2efc05fca4a4 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 26 Nov 2025 10:24:44 +0000 Subject: [PATCH 06/14] feat: Add interactive menu and comprehensive README for examples Reorganized the Example project to make all examples easily accessible: - Added interactive menu in Program.cs to choose which example to run - Support for command-line arguments to run specific examples directly - Moved original example code to TraditionalExample.cs - Renamed Main() to Run() in MultipleClientsExample and DifferentConfigsExample - Created comprehensive README.md with: - How to run each example (interactive or command-line) - Detailed overview of all 7 examples - When to use each pattern - Prerequisites and troubleshooting - Common patterns for ASP.NET Core and background services Examples now available: 1. Traditional - Basic usage with Connect.Local() 2. Dependency Injection - ASP.NET Core-style DI 3. Multiple Clients - Working with multiple named clients 4. Different Configs - Per-client custom settings 5. Configuration - Reading from appsettings.json 6. Lazy Initialization - On-demand client creation 7. Connect Helper - Backward compatibility Run interactively: dotnet run --project src/Example Run specific: dotnet run --project src/Example -- di --- src/Example/DifferentConfigsExample.cs | 4 +- src/Example/MultipleClientsExample.cs | 4 +- src/Example/Program.cs | 266 ++++++----------- src/Example/README.md | 386 +++++++++++++++++++++++++ src/Example/TraditionalExample.cs | 204 +++++++++++++ 5 files changed, 676 insertions(+), 188 deletions(-) create mode 100644 src/Example/README.md create mode 100644 src/Example/TraditionalExample.cs diff --git a/src/Example/DifferentConfigsExample.cs b/src/Example/DifferentConfigsExample.cs index c3865628..275232d1 100644 --- a/src/Example/DifferentConfigsExample.cs +++ b/src/Example/DifferentConfigsExample.cs @@ -11,9 +11,9 @@ namespace Example; /// public class DifferentConfigsExample { - public static async Task Main(string[] args) + public static async Task Run() { - var host = Host.CreateDefaultBuilder(args) + var host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { // Client 1: Production cloud with SSL and API key diff --git a/src/Example/MultipleClientsExample.cs b/src/Example/MultipleClientsExample.cs index 4f988798..f0f96ee2 100644 --- a/src/Example/MultipleClientsExample.cs +++ b/src/Example/MultipleClientsExample.cs @@ -11,9 +11,9 @@ namespace Example; /// public class MultipleClientsExample { - public static async Task Main(string[] args) + public static async Task Run() { - var host = Host.CreateDefaultBuilder(args) + var host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { // Register multiple named Weaviate clients diff --git a/src/Example/Program.cs b/src/Example/Program.cs index ff87e679..10beace5 100644 --- a/src/Example/Program.cs +++ b/src/Example/Program.cs @@ -1,200 +1,98 @@ -using System.Text.Json; -using Weaviate.Client; -using Weaviate.Client.Models; - -namespace Example; +using Example; class Program { - private static readonly bool _useBatchInsert = true; - - private record CatDataWithVectors(float[] Vector, Cat Data); - - static async Task> GetCatsAsync(string filename) + static async Task Main(string[] args) { - try + if (args.Length > 0) { - if (!File.Exists(filename)) - { - Console.WriteLine($"File not found: {filename}"); - return []; // Return an empty list if the file doesn't exist - } + await RunExampleByName(args[0]); + return; + } - using FileStream fs = new FileStream( - filename, - FileMode.Open, - FileAccess.Read, - FileShare.Read, - bufferSize: 4096, - useAsync: true - ); + Console.WriteLine("=== Weaviate C# Client Examples ===\n"); + Console.WriteLine("Choose an example to run:"); + Console.WriteLine(" 1. Traditional Example (cats.json batch insert)"); + Console.WriteLine(" 2. Dependency Injection Example"); + Console.WriteLine(" 3. Multiple Clients Example"); + Console.WriteLine(" 4. Different Configs Example"); + Console.WriteLine(" 5. Configuration Example (from appsettings.json)"); + Console.WriteLine(" 6. Lazy Initialization Example"); + Console.WriteLine(" 7. Connect Helper Example"); + Console.WriteLine(); + Console.Write("Enter your choice (1-7): "); - // Deserialize directly from the stream for better performance, especially with large files - var data = await JsonSerializer.DeserializeAsync>(fs) ?? []; + var choice = Console.ReadLine(); - return data; - } - catch (JsonException ex) - { - Console.WriteLine($"Error deserializing JSON: {ex.Message}"); - return []; // Return an empty list on deserialization error - } - catch (Exception ex) - { - Console.WriteLine($"An unexpected error occurred: {ex.Message}"); - return []; // Return an empty list on any other error - } + await RunExampleByChoice(choice); } - static async Task Main() + static async Task RunExampleByChoice(string? choice) { - // Read 250 cats from JSON file and unmarshal into Cat class - var cats = await GetCatsAsync("cats.json"); - - // Use the C# client to store all cats with a cat class - Console.WriteLine("Cats to store: " + cats.Count); - - WeaviateClient weaviate = await Connect.Local(); - - var collection = await weaviate.Collections.Use("Cat"); - - // Should throw CollectionNotFound - try - { - var collectionNotFound = await collection.Config.Get(); - } - catch - { - Console.WriteLine("cat collection not found"); + switch (choice) + { + case "1": + await TraditionalExample.Run(); + break; + case "2": + await DependencyInjectionExample.Main([]); + break; + case "3": + await MultipleClientsExample.Run(); + break; + case "4": + await DifferentConfigsExample.Run(); + break; + case "5": + await ConfigurationExample.RunAsync(); + break; + case "6": + await LazyInitializationExample.RunAsync(); + break; + case "7": + await ConnectHelperExample.RunAsync(); + break; + default: + Console.WriteLine("Invalid choice. Running traditional example..."); + await TraditionalExample.Run(); + break; } + } - // Delete any existing "cat" class - try - { - await collection.Delete(); - Console.WriteLine("Deleted existing 'Cat' collection"); - } - catch (Exception e) - { - Console.WriteLine($"Error deleting collections: {e.Message}"); - } - - var catCollection = new CollectionConfig() - { - Name = "Cat", - Description = "Lots of Cats of multiple breeds", - Properties = Property.FromClass(), - VectorConfig = new VectorConfig("default", new Vectorizer.Text2VecWeaviate()), - }; - - collection = await weaviate.Collections.Create(catCollection); - - await foreach (var c in weaviate.Collections.List()) - { - Console.WriteLine($"Collection: {c.Name}"); - } - - if (_useBatchInsert) - { - // Batch Insertion Demo - var requests = cats.Select(c => (c.Data, new Vectors { { "default", c.Vector } })); - - var batchInsertions = await collection.Data.InsertMany(requests); - } - else - { - // Normal Insertion Demo - foreach (var cat in cats) - { - var vectors = new Vectors() { { "default", cat.Vector } }; - - var inserted = await collection.Data.Insert(cat.Data, vectors: vectors); - } - } - - // Get all objects and sum up the counter property - var result = await collection.Query.FetchObjects(limit: 250); - var retrieved = result.Objects.ToList(); - Console.WriteLine("Cats retrieved: " + retrieved.Count()); - var sum = retrieved.Sum(c => c.Object?.Counter ?? 0); - - // Delete object - var firstObj = retrieved.First(); - if (firstObj.ID is Guid id) - { - await collection.Data.DeleteByID(id); - } - - result = await collection.Query.FetchObjects(limit: 5); - retrieved = result.Objects.ToList(); - Console.WriteLine("Cats retrieved: " + retrieved.Count()); - - firstObj = retrieved.First(); - if (firstObj.ID is Guid id2) - { - var fetched = await collection.Query.FetchObjectByID(id: id2); - Console.WriteLine( - "Cat retrieved via gRPC matches: " + ((fetched?.ID ?? Guid.Empty) == id2) - ); - } - - { - var idList = retrieved - .Where(c => c.ID.HasValue) - .Take(10) - .Select(c => c.ID!.Value) - .ToHashSet(); - - var fetched = await collection.Query.FetchObjectsByIDs(idList); - Console.WriteLine( - $"Cats retrieved via gRPC matches:{Environment.NewLine} {JsonSerializer.Serialize(fetched.Objects, new JsonSerializerOptions { WriteIndented = true })}" - ); - } - - Console.WriteLine("Querying Neighboring Cats: [20,21,22]"); - - var queryNearVector = await collection.Query.NearVector( - vector: new[] { 20f, 21f, 22f }, - distance: 0.5f, - limit: 5, - returnProperties: ["name", "breed", "color", "counter"], - returnMetadata: MetadataOptions.Score | MetadataOptions.Distance - ); - - foreach (var cat in queryNearVector.Objects.Select(o => o.Object)) - { - // Console.WriteLine( - // JsonSerializer.Serialize(cat, new JsonSerializerOptions { WriteIndented = true }) - // ); - - Console.WriteLine(cat); - } - - Console.WriteLine(); - Console.WriteLine("Using collection iterator:"); - - // Cursor API demo - var objects = collection.Iterator().Select(o => o.Object); - var sumWithIterator = await objects.SumAsync(c => c!.Counter); - - // Print all cats found - foreach (var cat in await objects.OrderBy(x => x!.Counter).ToListAsync()) - { - Console.WriteLine(cat); - } - - Console.WriteLine($"Sum of counter on cats: {sumWithIterator}"); - - var sphynxQuery = await collection.Query.BM25( - query: "Sphynx", - returnMetadata: MetadataOptions.Score - ); - - Console.WriteLine(); - Console.WriteLine("Querying Cat Breed: Sphynx"); - foreach (var cat in sphynxQuery) - { - Console.WriteLine(cat); + static async Task RunExampleByName(string name) + { + switch (name.ToLower()) + { + case "traditional": + await TraditionalExample.Run(); + break; + case "di": + case "dependency-injection": + await DependencyInjectionExample.Main([]); + break; + case "multiple": + case "multiple-clients": + await MultipleClientsExample.Run(); + break; + case "configs": + case "different-configs": + await DifferentConfigsExample.Run(); + break; + case "configuration": + await ConfigurationExample.RunAsync(); + break; + case "lazy": + case "lazy-init": + await LazyInitializationExample.RunAsync(); + break; + case "connect": + case "connect-helper": + await ConnectHelperExample.RunAsync(); + break; + default: + Console.WriteLine($"Unknown example: {name}"); + Console.WriteLine("Available examples: traditional, di, multiple, configs, configuration, lazy, connect"); + break; } } } diff --git a/src/Example/README.md b/src/Example/README.md new file mode 100644 index 00000000..8d513b9f --- /dev/null +++ b/src/Example/README.md @@ -0,0 +1,386 @@ +# Weaviate C# Client Examples + +This project contains multiple examples demonstrating different ways to use the Weaviate C# client. + +## Running the Examples + +### Interactive Menu + +Run without arguments to see an interactive menu: + +```bash +dotnet run --project src/Example +``` + +You'll see: +``` +=== Weaviate C# Client Examples === + +Choose an example to run: + 1. Traditional Example (cats.json batch insert) + 2. Dependency Injection Example + 3. Multiple Clients Example + 4. Different Configs Example + 5. Configuration Example (from appsettings.json) + 6. Lazy Initialization Example + 7. Connect Helper Example + +Enter your choice (1-7): +``` + +### Command Line + +Run a specific example by name: + +```bash +dotnet run --project src/Example -- traditional +dotnet run --project src/Example -- di +dotnet run --project src/Example -- multiple +dotnet run --project src/Example -- configs +dotnet run --project src/Example -- configuration +dotnet run --project src/Example -- lazy +dotnet run --project src/Example -- connect +``` + +## Examples Overview + +### 1. Traditional Example + +**File**: `TraditionalExample.cs` + +Classic usage without dependency injection. Demonstrates: +- Using `Connect.Local()` to create a client +- Batch insertion of data +- Basic queries (FetchObjects, FetchObjectByID, FetchObjectsByIDs) +- Vector similarity search with NearVector +- BM25 full-text search +- Iterator pattern for large result sets + +**When to use**: Simple scripts, console applications, quick prototypes. + +```csharp +var client = await Connect.Local(); +var collection = client.Collections.Use("Cat"); +var results = await collection.Query.FetchObjects(limit: 10); +``` + +--- + +### 2. Dependency Injection Example + +**File**: `DependencyInjectionExample.cs` + +Modern ASP.NET Core-style dependency injection with eager initialization. + +**What it shows**: +- Registering Weaviate with `AddWeaviateLocal()` +- Eager initialization via `IHostedService` (client initializes on app startup) +- Injecting `WeaviateClient` into services +- Using ILogger for structured logging + +**When to use**: ASP.NET Core applications, web APIs, long-running services. + +```csharp +// Startup +services.AddWeaviateLocal( + hostname: "localhost", + restPort: 8080, + grpcPort: 50051, + eagerInitialization: true +); + +// In your service +public class CatService +{ + private readonly WeaviateClient _weaviate; + + public CatService(WeaviateClient weaviate) + { + _weaviate = weaviate; // Already initialized! + } +} +``` + +--- + +### 3. Multiple Clients Example + +**File**: `MultipleClientsExample.cs` + +Using multiple named Weaviate clients simultaneously. + +**What it shows**: +- Registering multiple clients with different configurations +- Using `IWeaviateClientFactory` to get clients by name +- Multi-environment scenarios (prod, staging, local) +- Syncing data between environments + +**When to use**: +- Multi-region deployments +- Multi-tenant architectures +- Different databases for analytics vs operations +- Working with multiple Weaviate clusters + +```csharp +// Register multiple clients +services.AddWeaviateClient("production", options => { ... }); +services.AddWeaviateClient("staging", options => { ... }); +services.AddWeaviateClient("local", "localhost", 8080, 50051); + +// Use in service +public class MyService +{ + private readonly IWeaviateClientFactory _factory; + + public async Task ProcessAsync() + { + var prodClient = await _factory.GetClientAsync("production"); + var stagingClient = await _factory.GetClientAsync("staging"); + // Each has independent configuration + } +} +``` + +--- + +### 4. Different Configs Example + +**File**: `DifferentConfigsExample.cs` + +Demonstrates how each named client can have completely different settings. + +**What it shows**: +- Different hosts and ports per client +- Different SSL settings +- Different authentication (API key, OAuth, OIDC, none) +- Different timeouts per client +- Custom headers per client +- Different retry policies + +**When to use**: When you need fine-grained control over each client's behavior. + +```csharp +// Production: SSL + API key + short timeouts +services.AddWeaviateClient("production", options => +{ + options.RestEndpoint = "prod.weaviate.cloud"; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("key"); + options.QueryTimeout = TimeSpan.FromSeconds(30); +}); + +// Local: No SSL, no auth, long timeouts for debugging +services.AddWeaviateClient("local", options => +{ + options.RestEndpoint = "localhost"; + options.UseSsl = false; + options.Credentials = null; + options.QueryTimeout = TimeSpan.FromMinutes(5); +}); +``` + +--- + +### 5. Configuration Example + +**File**: `DependencyInjectionExample.cs` → `ConfigurationExample` class + +Reading Weaviate settings from `appsettings.json`. + +**What it shows**: +- Using `IConfiguration` with `AddWeaviate()` +- Externalizing connection settings +- Environment-specific configuration + +**When to use**: Production applications where settings should be configurable without code changes. + +**appsettings.json**: +```json +{ + "Weaviate": { + "RestEndpoint": "localhost", + "RestPort": 8080, + "GrpcPort": 50051, + "UseSsl": false + } +} +``` + +**Code**: +```csharp +services.AddWeaviate( + context.Configuration.GetSection("Weaviate"), + eagerInitialization: true +); +``` + +--- + +### 6. Lazy Initialization Example + +**File**: `DependencyInjectionExample.cs` → `LazyInitializationExample` class + +Client initializes on first use instead of startup. + +**What it shows**: +- Setting `eagerInitialization: false` +- Manually triggering initialization with `InitializeAsync()` +- Checking initialization status with `IsInitialized` + +**When to use**: +- Applications where Weaviate might not always be needed +- Faster startup times +- On-demand client creation + +```csharp +services.AddWeaviateLocal(eagerInitialization: false); + +// Later... +var client = serviceProvider.GetRequiredService(); +Console.WriteLine(client.IsInitialized); // False + +await client.InitializeAsync(); // Initialize now +Console.WriteLine(client.IsInitialized); // True +``` + +--- + +### 7. Connect Helper Example + +**File**: `DependencyInjectionExample.cs` → `ConnectHelperExample` class + +Shows that traditional `Connect.Local()` and `Connect.Cloud()` still work. + +**What it shows**: +- Backward compatibility +- Fully async, no blocking calls +- Simple one-liner client creation + +**When to use**: Quick scripts, simple applications, backward compatibility. + +```csharp +// Local +var client = await Connect.Local(); + +// Cloud +var client = await Connect.Cloud( + clusterEndpoint: "my-cluster.weaviate.cloud", + apiKey: "my-api-key" +); +``` + +--- + +## Prerequisites + +All examples require: +- A running Weaviate instance (default: `localhost:8080`) +- .NET 8.0 or later + +### Starting Weaviate Locally + +```bash +docker run -d \ + -p 8080:8080 \ + -p 50051:50051 \ + -e ENABLE_MODULES=text2vec-weaviate \ + -e DEFAULT_VECTORIZER_MODULE=text2vec-weaviate \ + cr.weaviate.io/semitechnologies/weaviate:latest +``` + +## Example Selection Guide + +| Scenario | Recommended Example | +|----------|-------------------| +| Quick script or console app | Traditional (#1) or Connect Helper (#7) | +| ASP.NET Core web API | Dependency Injection (#2) | +| Multiple Weaviate clusters | Multiple Clients (#3) | +| Per-client custom settings | Different Configs (#4) | +| Configuration-driven setup | Configuration (#5) | +| Faster startup, on-demand init | Lazy Initialization (#6) | +| Learning the basics | Traditional (#1) | +| Production applications | Dependency Injection (#2) + Configuration (#5) | + +## Common Patterns + +### ASP.NET Core Web API + +```csharp +// Program.cs or Startup.cs +builder.Services.AddWeaviateLocal( + hostname: builder.Configuration["Weaviate:Host"] ?? "localhost", + restPort: ushort.Parse(builder.Configuration["Weaviate:Port"] ?? "8080"), + eagerInitialization: true +); + +// Controller +[ApiController] +[Route("[controller]")] +public class SearchController : ControllerBase +{ + private readonly WeaviateClient _weaviate; + + public SearchController(WeaviateClient weaviate) + { + _weaviate = weaviate; + } + + [HttpGet] + public async Task Search([FromQuery] string query) + { + var collection = _weaviate.Collections.Use("Product"); + var results = await collection.Query.BM25(query, limit: 10); + return Ok(results.Objects); + } +} +``` + +### Background Service + +```csharp +public class WeaviateBackgroundService : BackgroundService +{ + private readonly WeaviateClient _weaviate; + private readonly ILogger _logger; + + public WeaviateBackgroundService( + WeaviateClient weaviate, + ILogger logger) + { + _weaviate = weaviate; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + // Process data periodically + await ProcessDataAsync(); + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + } + } +} +``` + +## Documentation + +For more information: +- **DEPENDENCY_INJECTION.md** - Comprehensive DI guide +- **MULTIPLE_CLIENTS.md** - Multi-client patterns and use cases +- [Official Weaviate Docs](https://weaviate.io/developers/weaviate) + +## Troubleshooting + +**Can't connect to Weaviate**: +- Ensure Weaviate is running: `docker ps` +- Check ports are correct: 8080 (REST), 50051 (gRPC) +- Verify endpoint: `curl http://localhost:8080/v1/.well-known/ready` + +**Build errors about `Host`**: +- Ensure `Microsoft.Extensions.Hosting` package is referenced in `Example.csproj` + +**Initialization timeout**: +- Increase `initTimeout` in options +- Check network connectivity to Weaviate +- Verify Weaviate is fully started and healthy diff --git a/src/Example/TraditionalExample.cs b/src/Example/TraditionalExample.cs new file mode 100644 index 00000000..585cfdbd --- /dev/null +++ b/src/Example/TraditionalExample.cs @@ -0,0 +1,204 @@ +using System.Text.Json; +using Weaviate.Client; +using Weaviate.Client.Models; + +namespace Example; + +/// +/// Traditional example showing basic Weaviate operations without dependency injection. +/// Demonstrates batch insert, queries, near vector search, BM25, and iterator usage. +/// +public class TraditionalExample +{ + private static readonly bool _useBatchInsert = true; + + private record CatDataWithVectors(float[] Vector, Cat Data); + + static async Task> GetCatsAsync(string filename) + { + try + { + if (!File.Exists(filename)) + { + Console.WriteLine($"File not found: {filename}"); + return []; // Return an empty list if the file doesn't exist + } + + using FileStream fs = new FileStream( + filename, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 4096, + useAsync: true + ); + + // Deserialize directly from the stream for better performance, especially with large files + var data = await JsonSerializer.DeserializeAsync>(fs) ?? []; + + return data; + } + catch (JsonException ex) + { + Console.WriteLine($"Error deserializing JSON: {ex.Message}"); + return []; // Return an empty list on deserialization error + } + catch (Exception ex) + { + Console.WriteLine($"An unexpected error occurred: {ex.Message}"); + return []; // Return an empty list on any other error + } + } + + public static async Task Run() + { + Console.WriteLine("=== Traditional Example ===\n"); + + // Read 250 cats from JSON file and unmarshal into Cat class + var cats = await GetCatsAsync("cats.json"); + + // Use the C# client to store all cats with a cat class + Console.WriteLine("Cats to store: " + cats.Count); + + WeaviateClient weaviate = await Connect.Local(); + + var collection = await weaviate.Collections.Use("Cat"); + + // Should throw CollectionNotFound + try + { + var collectionNotFound = await collection.Config.Get(); + } + catch + { + Console.WriteLine("cat collection not found"); + } + + // Delete any existing "cat" class + try + { + await collection.Delete(); + Console.WriteLine("Deleted existing 'Cat' collection"); + } + catch (Exception e) + { + Console.WriteLine($"Error deleting collections: {e.Message}"); + } + + var catCollection = new CollectionConfig() + { + Name = "Cat", + Description = "Lots of Cats of multiple breeds", + Properties = Property.FromClass(), + VectorConfig = new VectorConfig("default", new Vectorizer.Text2VecWeaviate()), + }; + + collection = await weaviate.Collections.Create(catCollection); + + await foreach (var c in weaviate.Collections.List()) + { + Console.WriteLine($"Collection: {c.Name}"); + } + + if (_useBatchInsert) + { + // Batch Insertion Demo + var requests = cats.Select(c => (c.Data, new Vectors { { "default", c.Vector } })); + + var batchInsertions = await collection.Data.InsertMany(requests); + } + else + { + // Normal Insertion Demo + foreach (var cat in cats) + { + var vectors = new Vectors() { { "default", cat.Vector } }; + + var inserted = await collection.Data.Insert(cat.Data, vectors: vectors); + } + } + + // Get all objects and sum up the counter property + var result = await collection.Query.FetchObjects(limit: 250); + var retrieved = result.Objects.ToList(); + Console.WriteLine("Cats retrieved: " + retrieved.Count()); + var sum = retrieved.Sum(c => c.Object?.Counter ?? 0); + + // Delete object + var firstObj = retrieved.First(); + if (firstObj.ID is Guid id) + { + await collection.Data.DeleteByID(id); + } + + result = await collection.Query.FetchObjects(limit: 5); + retrieved = result.Objects.ToList(); + Console.WriteLine("Cats retrieved: " + retrieved.Count()); + + firstObj = retrieved.First(); + if (firstObj.ID is Guid id2) + { + var fetched = await collection.Query.FetchObjectByID(id: id2); + Console.WriteLine( + "Cat retrieved via gRPC matches: " + ((fetched?.ID ?? Guid.Empty) == id2) + ); + } + + { + var idList = retrieved + .Where(c => c.ID.HasValue) + .Take(10) + .Select(c => c.ID!.Value) + .ToHashSet(); + + var fetched = await collection.Query.FetchObjectsByIDs(idList); + Console.WriteLine( + $"Cats retrieved via gRPC matches:{Environment.NewLine} {JsonSerializer.Serialize(fetched.Objects, new JsonSerializerOptions { WriteIndented = true })}" + ); + } + + Console.WriteLine("Querying Neighboring Cats: [20,21,22]"); + + var queryNearVector = await collection.Query.NearVector( + vector: new[] { 20f, 21f, 22f }, + distance: 0.5f, + limit: 5, + returnProperties: ["name", "breed", "color", "counter"], + returnMetadata: MetadataOptions.Score | MetadataOptions.Distance + ); + + foreach (var cat in queryNearVector.Objects.Select(o => o.Object)) + { + Console.WriteLine(cat); + } + + Console.WriteLine(); + Console.WriteLine("Using collection iterator:"); + + // Cursor API demo + var objects = collection.Iterator().Select(o => o.Object); + var sumWithIterator = await objects.SumAsync(c => c!.Counter); + + // Print all cats found + foreach (var cat in await objects.OrderBy(x => x!.Counter).ToListAsync()) + { + Console.WriteLine(cat); + } + + Console.WriteLine($"Sum of counter on cats: {sumWithIterator}"); + + var sphynxQuery = await collection.Query.BM25( + query: "Sphynx", + returnMetadata: MetadataOptions.Score + ); + + Console.WriteLine(); + Console.WriteLine("Querying Cat Breed: Sphynx"); + foreach (var cat in sphynxQuery) + { + Console.WriteLine(cat); + } + + weaviate.Dispose(); + } +} From 0ffb6780faeb3e20d73cb2f1b5b3515fefd56a8c Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 26 Nov 2025 09:44:18 +0000 Subject: [PATCH 07/14] fix: Add Microsoft.Extensions.Hosting package to Example project The DependencyInjectionExample requires Microsoft.Extensions.Hosting for Host.CreateDefaultBuilder() but the package reference was missing from the project file. --- src/Example/Example.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Example/Example.csproj b/src/Example/Example.csproj index 43276c8c..c98542ec 100644 --- a/src/Example/Example.csproj +++ b/src/Example/Example.csproj @@ -11,6 +11,7 @@ + From 677db06b1ec4effc264789e34cb35b815e32e1d0 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 26 Nov 2025 16:20:00 +0100 Subject: [PATCH 08/14] Dependency Injection baseline work --- src/Example/DependencyInjectionExample.cs | 120 +++--- src/Example/DifferentConfigsExample.cs | 195 +++++---- src/Example/MultipleClientsExample.cs | 119 +++--- src/Example/Program.cs | 8 +- src/Example/TraditionalExample.cs | 2 +- src/Example/packages.lock.json | 162 ++++++- src/Weaviate.Client.Tests/packages.lock.json | 22 + src/Weaviate.Client/Auth.cs | 59 +++ src/Weaviate.Client/ClientConfiguration.cs | 89 ++++ src/Weaviate.Client/ClusterClient.cs | 17 +- src/Weaviate.Client/CollectionsClient.cs | 19 +- .../DefaultTokenServiceFactory.cs | 136 ++++++ .../WeaviateClientFactory.cs | 22 +- .../WeaviateServiceCollectionExtensions.cs | 232 +++++----- src/Weaviate.Client/ITokenServiceFactory.cs | 7 + .../Typed/TypedCollectionClient.cs | 20 - .../Validation/ValidationExtensions.cs | 49 ++- src/Weaviate.Client/WeaviateClient.cs | 403 ++---------------- src/Weaviate.Client/WeaviateDefaults.cs | 36 ++ src/Weaviate.Client/packages.lock.json | 40 +- 20 files changed, 1029 insertions(+), 728 deletions(-) create mode 100644 src/Weaviate.Client/Auth.cs create mode 100644 src/Weaviate.Client/ClientConfiguration.cs create mode 100644 src/Weaviate.Client/DefaultTokenServiceFactory.cs create mode 100644 src/Weaviate.Client/ITokenServiceFactory.cs create mode 100644 src/Weaviate.Client/WeaviateDefaults.cs diff --git a/src/Example/DependencyInjectionExample.cs b/src/Example/DependencyInjectionExample.cs index d4b101b9..d2d89c02 100644 --- a/src/Example/DependencyInjectionExample.cs +++ b/src/Example/DependencyInjectionExample.cs @@ -11,23 +11,25 @@ namespace Example; /// public class DependencyInjectionExample { - public static async Task Main(string[] args) + public static async Task Run() { // Build host with dependency injection - var host = Host.CreateDefaultBuilder(args) - .ConfigureServices((context, services) => - { - // Register Weaviate client - services.AddWeaviateLocal( - hostname: "localhost", - restPort: 8080, - grpcPort: 50051, - eagerInitialization: true // Client initializes on startup - ); - - // Register your services that use Weaviate - services.AddSingleton(); - }) + var host = Host.CreateDefaultBuilder() + .ConfigureServices( + (context, services) => + { + // Register Weaviate client + services.AddWeaviateLocal( + hostname: "localhost", + restPort: 8080, + grpcPort: 50051, + eagerInitialization: true // Client initializes on startup + ); + + // Register your services that use Weaviate + services.AddSingleton(); + } + ) .Build(); // Run the host - this triggers eager initialization @@ -61,7 +63,8 @@ public CatService(WeaviateClient weaviate, ILogger logger) // Client is already initialized! _logger.LogInformation( "CatService created. Weaviate version: {Version}", - _weaviate.WeaviateVersion); + _weaviate.WeaviateVersion + ); } public async Task DemonstrateUsageAsync() @@ -70,7 +73,11 @@ public async Task DemonstrateUsageAsync() _logger.LogInformation("Client initialized: {IsInitialized}", _weaviate.IsInitialized); // Create or get collection - var collection = _weaviate.Collections.Use("Cat"); + var collection = await _weaviate.Collections.Use("Cat").ValidateTypeOrThrow(); + // or also: + // var collection = await _weaviate.Collections.Use("Cat", validateType: true); + // or the yolo way: + // var collection = _weaviate.Collections.Use("Cat"); try { @@ -82,26 +89,31 @@ public async Task DemonstrateUsageAsync() { // Create collection _logger.LogInformation("Creating Cat collection..."); - await _weaviate.Collections.Create(new Weaviate.Client.Models.CollectionConfig - { - Name = "Cat", - Description = "Example cat collection for DI demo", - Properties = Weaviate.Client.Models.Property.FromClass(), - VectorConfig = new Weaviate.Client.Models.VectorConfig( - "default", - new Weaviate.Client.Models.Vectorizer.Text2VecWeaviate()) - }); + await _weaviate.Collections.Create( + new Weaviate.Client.Models.CollectionConfig + { + Name = "Cat", + Description = "Example cat collection for DI demo", + Properties = Weaviate.Client.Models.Property.FromClass(), + VectorConfig = new Weaviate.Client.Models.VectorConfig( + "default", + new Weaviate.Client.Models.Vectorizer.Text2VecWeaviate() + ), + } + ); } // Insert a cat _logger.LogInformation("Inserting a cat..."); - var catId = await collection.Data.Insert(new Cat - { - Name = "Fluffy", - Breed = "Persian", - Color = "white", - Counter = 1 - }); + var catId = await collection.Data.Insert( + new Cat + { + Name = "Fluffy", + Breed = "Persian", + Color = "white", + Counter = 1, + } + ); _logger.LogInformation("Inserted cat with ID: {Id}", catId); @@ -113,8 +125,12 @@ await _weaviate.Collections.Create(new Weaviate.Client.Models.CollectionCon foreach (var obj in results.Objects) { - var cat = obj.As(); - _logger.LogInformation(" - {Name} ({Breed}, {Color})", cat?.Name, cat?.Breed, cat?.Color); + _logger.LogInformation( + " - {Name} ({Breed}, {Color})", + obj.Object?.Name, + obj.Object?.Breed, + obj.Object?.Color + ); } // Cleanup @@ -131,16 +147,18 @@ public class ConfigurationExample public static async Task RunAsync() { var host = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Register from configuration section - services.AddWeaviate( - context.Configuration.GetSection("Weaviate"), - eagerInitialization: true - ); - - services.AddSingleton(); - }) + .ConfigureServices( + (context, services) => + { + // Register from configuration section + services.AddWeaviate( + context.Configuration.GetSection("Weaviate"), + eagerInitialization: true + ); + + services.AddSingleton(); + } + ) .Build(); await host.StartAsync(); @@ -160,11 +178,13 @@ public class LazyInitializationExample public static async Task RunAsync() { var host = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Lazy initialization - client initializes on first use - services.AddWeaviateLocal(eagerInitialization: false); - }) + .ConfigureServices( + (context, services) => + { + // Lazy initialization - client initializes on first use + services.AddWeaviateLocal(eagerInitialization: false); + } + ) .Build(); await host.StartAsync(); diff --git a/src/Example/DifferentConfigsExample.cs b/src/Example/DifferentConfigsExample.cs index 275232d1..07c52841 100644 --- a/src/Example/DifferentConfigsExample.cs +++ b/src/Example/DifferentConfigsExample.cs @@ -14,88 +14,105 @@ public class DifferentConfigsExample public static async Task Run() { var host = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Client 1: Production cloud with SSL and API key - services.AddWeaviateClient("production", options => + .ConfigureServices( + (context, services) => { - options.RestEndpoint = "prod.weaviate.cloud"; - options.GrpcEndpoint = "grpc-prod.weaviate.cloud"; - options.RestPort = 443; - options.GrpcPort = 443; - options.UseSsl = true; - options.Credentials = Auth.ApiKey("prod-api-key-here"); - options.DefaultTimeout = TimeSpan.FromSeconds(60); - options.QueryTimeout = TimeSpan.FromSeconds(30); - options.Headers = new Dictionary - { - ["X-Environment"] = "production" - }; - }); - - // Client 2: Local development, no SSL, no auth, longer timeouts - services.AddWeaviateClient("local", options => - { - options.RestEndpoint = "localhost"; - options.GrpcEndpoint = "localhost"; - options.RestPort = 8080; - options.GrpcPort = 50051; - options.UseSsl = false; - options.Credentials = null; // No auth for local - options.DefaultTimeout = TimeSpan.FromSeconds(120); // Longer for debugging - options.QueryTimeout = TimeSpan.FromSeconds(300); // Very long for local testing - }); - - // Client 3: Staging with OAuth credentials - services.AddWeaviateClient("staging", options => - { - options.RestEndpoint = "staging.weaviate.cloud"; - options.GrpcEndpoint = "grpc-staging.weaviate.cloud"; - options.RestPort = 443; - options.GrpcPort = 443; - options.UseSsl = true; - options.Credentials = Auth.ClientCredentials( - "staging-client-secret", - "weaviate.read", - "weaviate.write" + // Client 1: Production cloud with SSL and API key + services.AddWeaviateClient( + "production", + options => + { + options.RestEndpoint = "prod.weaviate.cloud"; + options.GrpcEndpoint = "grpc-prod.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("prod-api-key-here"); + options.DefaultTimeout = TimeSpan.FromSeconds(60); + options.QueryTimeout = TimeSpan.FromSeconds(30); + options.Headers = new Dictionary + { + ["X-Environment"] = "production", + }; + } ); - options.DefaultTimeout = TimeSpan.FromSeconds(45); - }); - // Client 4: Analytics cluster with custom ports and retry policy - services.AddWeaviateClient("analytics", options => - { - options.RestEndpoint = "analytics.internal.company.com"; - options.GrpcEndpoint = "analytics.internal.company.com"; - options.RestPort = 9090; // Custom port - options.GrpcPort = 9091; // Custom port - options.UseSsl = true; - options.Credentials = Auth.ApiKey("analytics-key"); - options.QueryTimeout = TimeSpan.FromSeconds(120); // Slow analytics queries - options.RetryPolicy = new RetryPolicy - { - MaxRetries = 5, // More retries for unreliable network - RetryDelay = TimeSpan.FromSeconds(2) - }; - }); - - // Client 5: Legacy system with password auth - services.AddWeaviateClient("legacy", options => - { - options.RestEndpoint = "legacy.oldserver.com"; - options.GrpcEndpoint = "legacy.oldserver.com"; - options.RestPort = 8081; - options.GrpcPort = 50052; - options.UseSsl = false; // Old server doesn't support SSL - options.Credentials = Auth.ClientPassword( - "legacy-username", - "legacy-password" + // Client 2: Local development, no SSL, no auth, longer timeouts + services.AddWeaviateClient( + "local", + options => + { + options.RestEndpoint = "localhost"; + options.GrpcEndpoint = "localhost"; + options.RestPort = 8080; + options.GrpcPort = 50051; + options.UseSsl = false; + options.Credentials = null; // No auth for local + options.DefaultTimeout = TimeSpan.FromSeconds(120); // Longer for debugging + options.QueryTimeout = TimeSpan.FromSeconds(300); // Very long for local testing + } + ); + + // Client 3: Staging with OAuth credentials + services.AddWeaviateClient( + "staging", + options => + { + options.RestEndpoint = "staging.weaviate.cloud"; + options.GrpcEndpoint = "grpc-staging.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ClientCredentials( + "staging-client-secret", + "weaviate.read", + "weaviate.write" + ); + options.DefaultTimeout = TimeSpan.FromSeconds(45); + } + ); + + // Client 4: Analytics cluster with custom ports and retry policy + services.AddWeaviateClient( + "analytics", + options => + { + options.RestEndpoint = "analytics.internal.company.com"; + options.GrpcEndpoint = "analytics.internal.company.com"; + options.RestPort = 9090; // Custom port + options.GrpcPort = 9091; // Custom port + options.UseSsl = true; + options.Credentials = Auth.ApiKey("analytics-key"); + options.QueryTimeout = TimeSpan.FromSeconds(120); // Slow analytics queries + options.RetryPolicy = new RetryPolicy + { + MaxRetries = 5, // More retries for unreliable network + InitialDelay = TimeSpan.FromSeconds(2), + }; + } + ); + + // Client 5: Legacy system with password auth + services.AddWeaviateClient( + "legacy", + options => + { + options.RestEndpoint = "legacy.oldserver.com"; + options.GrpcEndpoint = "legacy.oldserver.com"; + options.RestPort = 8081; + options.GrpcPort = 50052; + options.UseSsl = false; // Old server doesn't support SSL + options.Credentials = Auth.ClientPassword( + "legacy-username", + "legacy-password" + ); + options.InitTimeout = TimeSpan.FromSeconds(10); // Slow to start + } ); - options.InitTimeout = TimeSpan.FromSeconds(10); // Slow to start - }); - services.AddSingleton(); - }) + services.AddSingleton(); + } + ) .Build(); await host.StartAsync(); @@ -129,33 +146,45 @@ public async Task ShowDifferentConfigsAsync() // Show each client's configuration Console.WriteLine($"Production:"); - Console.WriteLine($" - Endpoint: {prodClient.Configuration.RestAddress}:{prodClient.Configuration.RestPort}"); + Console.WriteLine( + $" - Endpoint: {prodClient.Configuration.RestAddress}:{prodClient.Configuration.RestPort}" + ); Console.WriteLine($" - SSL: {prodClient.Configuration.UseSsl}"); Console.WriteLine($" - Version: {prodClient.WeaviateVersion}"); Console.WriteLine($" - Query Timeout: {prodClient.QueryTimeout}"); Console.WriteLine(); Console.WriteLine($"Local:"); - Console.WriteLine($" - Endpoint: {localClient.Configuration.RestAddress}:{localClient.Configuration.RestPort}"); + Console.WriteLine( + $" - Endpoint: {localClient.Configuration.RestAddress}:{localClient.Configuration.RestPort}" + ); Console.WriteLine($" - SSL: {localClient.Configuration.UseSsl}"); Console.WriteLine($" - Version: {localClient.WeaviateVersion}"); Console.WriteLine($" - Query Timeout: {localClient.QueryTimeout}"); Console.WriteLine(); Console.WriteLine($"Staging:"); - Console.WriteLine($" - Endpoint: {stagingClient.Configuration.RestAddress}:{stagingClient.Configuration.RestPort}"); + Console.WriteLine( + $" - Endpoint: {stagingClient.Configuration.RestAddress}:{stagingClient.Configuration.RestPort}" + ); Console.WriteLine($" - SSL: {stagingClient.Configuration.UseSsl}"); Console.WriteLine($" - Version: {stagingClient.WeaviateVersion}"); Console.WriteLine(); Console.WriteLine($"Analytics:"); - Console.WriteLine($" - Endpoint: {analyticsClient.Configuration.RestAddress}:{analyticsClient.Configuration.RestPort}"); - Console.WriteLine($" - Custom Ports: REST={analyticsClient.Configuration.RestPort}, gRPC={analyticsClient.Configuration.GrpcPort}"); + Console.WriteLine( + $" - Endpoint: {analyticsClient.Configuration.RestAddress}:{analyticsClient.Configuration.RestPort}" + ); + Console.WriteLine( + $" - Custom Ports: REST={analyticsClient.Configuration.RestPort}, gRPC={analyticsClient.Configuration.GrpcPort}" + ); Console.WriteLine($" - Version: {analyticsClient.WeaviateVersion}"); Console.WriteLine(); Console.WriteLine($"Legacy:"); - Console.WriteLine($" - Endpoint: {legacyClient.Configuration.RestAddress}:{legacyClient.Configuration.RestPort}"); + Console.WriteLine( + $" - Endpoint: {legacyClient.Configuration.RestAddress}:{legacyClient.Configuration.RestPort}" + ); Console.WriteLine($" - SSL: {legacyClient.Configuration.UseSsl}"); Console.WriteLine($" - Version: {legacyClient.WeaviateVersion}"); diff --git a/src/Example/MultipleClientsExample.cs b/src/Example/MultipleClientsExample.cs index f0f96ee2..93d416ef 100644 --- a/src/Example/MultipleClientsExample.cs +++ b/src/Example/MultipleClientsExample.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -14,37 +15,49 @@ public class MultipleClientsExample public static async Task Run() { var host = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Register multiple named Weaviate clients - services.AddWeaviateClient("production", options => + .ConfigureServices( + (context, services) => { - options.RestEndpoint = "prod.weaviate.cloud"; - options.GrpcEndpoint = "grpc-prod.weaviate.cloud"; - options.RestPort = 443; - options.GrpcPort = 443; - options.UseSsl = true; - options.Credentials = Auth.ApiKey("prod-api-key"); - }); - - services.AddWeaviateClient("staging", options => - { - options.RestEndpoint = "staging.weaviate.cloud"; - options.GrpcEndpoint = "grpc-staging.weaviate.cloud"; - options.RestPort = 443; - options.GrpcPort = 443; - options.UseSsl = true; - options.Credentials = Auth.ApiKey("staging-api-key"); - }); - - services.AddWeaviateClient("local", "localhost", 8080, 50051); - - // Or use helper methods - services.AddWeaviateCloudClient("analytics", "analytics.weaviate.cloud", "analytics-key"); - - // Register services that use multiple clients - services.AddSingleton(); - }) + // Register multiple named Weaviate clients + services.AddWeaviateClient( + "production", + options => + { + options.RestEndpoint = "prod.weaviate.cloud"; + options.GrpcEndpoint = "grpc-prod.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("prod-api-key"); + } + ); + + services.AddWeaviateClient( + "staging", + options => + { + options.RestEndpoint = "staging.weaviate.cloud"; + options.GrpcEndpoint = "grpc-staging.weaviate.cloud"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = Auth.ApiKey("staging-api-key"); + } + ); + + services.AddWeaviateLocal("local", "localhost", 8080, 50051); + + // Or use helper methods + services.AddWeaviateCloud( + "analytics", + "analytics.weaviate.cloud", + "analytics-key" + ); + + // Register services that use multiple clients + services.AddSingleton(); + } + ) .Build(); await host.StartAsync(); @@ -66,7 +79,8 @@ public class MultiDatabaseService public MultiDatabaseService( IWeaviateClientFactory clientFactory, - ILogger logger) + ILogger logger + ) { _clientFactory = clientFactory; _logger = logger; @@ -92,19 +106,22 @@ public async Task DemonstrateMultipleClientsAsync() private async Task SyncDataBetweenEnvironmentsAsync( WeaviateClient prodClient, - WeaviateClient stagingClient) + WeaviateClient stagingClient + ) { _logger.LogInformation("\nSyncing data from production to staging..."); - var prodCollection = prodClient.Collections.Use("Cat"); - var stagingCollection = stagingClient.Collections.Use("Cat"); + var prodCollection = await prodClient.Collections.Use("Cat").ValidateTypeOrThrow(); + var stagingCollection = await stagingClient + .Collections.Use("Cat") + .ValidateTypeOrThrow(); // Fetch from production var prodResults = await prodCollection.Query.FetchObjects(limit: 100); _logger.LogInformation("Found {Count} cats in production", prodResults.Objects.Count()); // Insert into staging - var cats = prodResults.Objects.Select(o => o.As()!); + var cats = prodResults.Objects.Select(o => o.Object!); foreach (var cat in cats) { await stagingCollection.Data.Insert(cat); @@ -173,21 +190,23 @@ public static async Task RunAsync() */ var host = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Register clients from configuration - services.AddWeaviateClient( - "production", - context.Configuration.GetSection("Weaviate:Production") - .Get()! - .RestEndpoint); - - services.AddWeaviateClient( - "staging", - context.Configuration.GetSection("Weaviate:Staging") - .Get()! - .RestEndpoint); - }) + .ConfigureServices( + (context, services) => + { + // Register clients from configuration + services.AddWeaviateClient( + "production", + options => + context.Configuration.GetSection("Weaviate:Production").Bind(options) + ); + + services.AddWeaviateClient( + "staging", + options => + context.Configuration.GetSection("Weaviate:Staging").Bind(options) + ); + } + ) .Build(); await host.StartAsync(); diff --git a/src/Example/Program.cs b/src/Example/Program.cs index 10beace5..251f5c88 100644 --- a/src/Example/Program.cs +++ b/src/Example/Program.cs @@ -35,7 +35,7 @@ static async Task RunExampleByChoice(string? choice) await TraditionalExample.Run(); break; case "2": - await DependencyInjectionExample.Main([]); + await DependencyInjectionExample.Run(); break; case "3": await MultipleClientsExample.Run(); @@ -68,7 +68,7 @@ static async Task RunExampleByName(string name) break; case "di": case "dependency-injection": - await DependencyInjectionExample.Main([]); + await DependencyInjectionExample.Run(); break; case "multiple": case "multiple-clients": @@ -91,7 +91,9 @@ static async Task RunExampleByName(string name) break; default: Console.WriteLine($"Unknown example: {name}"); - Console.WriteLine("Available examples: traditional, di, multiple, configs, configuration, lazy, connect"); + Console.WriteLine( + "Available examples: traditional, di, multiple, configs, configuration, lazy, connect" + ); break; } } diff --git a/src/Example/TraditionalExample.cs b/src/Example/TraditionalExample.cs index 585cfdbd..b22bc23c 100644 --- a/src/Example/TraditionalExample.cs +++ b/src/Example/TraditionalExample.cs @@ -62,7 +62,7 @@ public static async Task Run() WeaviateClient weaviate = await Connect.Local(); - var collection = await weaviate.Collections.Use("Cat"); + var collection = weaviate.Collections.Use("Cat"); // Should throw CollectionNotFound try diff --git a/src/Example/packages.lock.json b/src/Example/packages.lock.json index c4145533..e80869d8 100644 --- a/src/Example/packages.lock.json +++ b/src/Example/packages.lock.json @@ -2,6 +2,36 @@ "version": 1, "dependencies": { "net8.0": { + "Microsoft.Extensions.Hosting": { + "type": "Direct", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "wNmQWRCa83HYbpxQ3wH7xBn8oyGjONSj1k8svzrFUFyJMfg/Ja/g0NfI0p85wxlUxBh97A6ypmL8X5vVUA5y2Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Configuration.Binder": "9.0.0", + "Microsoft.Extensions.Configuration.CommandLine": "9.0.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "9.0.0", + "Microsoft.Extensions.Configuration.Json": "9.0.0", + "Microsoft.Extensions.Configuration.UserSecrets": "9.0.0", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Physical": "9.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Configuration": "9.0.0", + "Microsoft.Extensions.Logging.Console": "9.0.0", + "Microsoft.Extensions.Logging.Debug": "9.0.0", + "Microsoft.Extensions.Logging.EventLog": "9.0.0", + "Microsoft.Extensions.Logging.EventSource": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, "System.Linq.Async": { "type": "Direct", "requested": "[6.0.3, )", @@ -83,6 +113,59 @@ "Microsoft.Extensions.Configuration.Abstractions": "9.0.8" } }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qD+hdkBtR9Ps7AxfhTJCnoVakkadHgHlD1WRN0QHGHod+SDuca1ao1kF4G2rmpAz2AEKrE2N2vE8CCCZ+ILnNw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "v5R638eNMxksfXb7MFnkPwLPp+Ym4W/SIGNuoe8qFVVyvygQD5DdLusybmYSJEr9zc1UzWzim/ATKeIOVvOFDg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "4EK93Jcd2lQG4GY6PAw8jGss0ZzFP0vPc1J85mES5fKNuDTqgFXHba9onBw2s18fs3I4vdo2AWyfD1mPAxWSQQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Physical": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "WiTK0LrnsqmedrbzwL7f4ZUo+/wByqy2eKab39I380i2rd8ImfCRMrtkqJVGDmfqlkP/YzhckVOwPc5MPrSNpg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "System.Text.Json": "9.0.0" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "FShWw8OysquwV7wQHYkkz0VWsJSo6ETUu4h7tJRMtnG0uR+tzKOldhcO8xB1pGSOI3Ng6v3N1Q94YO8Rzq1P6A==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Configuration.Json": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Physical": "9.0.0" + } + }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "9.0.8", @@ -116,6 +199,41 @@ "System.Diagnostics.DiagnosticSource": "9.0.8" } }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "4zZbQ4w+hCMm9J+z5NOj3giIPT2MhZxx05HX/MGuAmDBbjOuXlYIIRN+t4V6OLxy5nXZIcXO+dQMB/OWubuDkw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3+ZUSpOSmie+o8NnLIRqCxSh65XL/ExU7JYnFOg58awDRlY3lVpZ9A369jkoZL1rpsq7LDhEfkn2ghhGaY1y5Q==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "jGFKZiXs2HNseK3NK/rfwHNNovER71jSj4BD1a/649ml9+h6oEtYd0GSALZDNW8jZ2Rh+oAeadOa6sagYW1F2A==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "WNrad20tySNCPe9aJUK7Wfwh+RiyLF+id02FKW8Qfc+HAzNQHazcqMXAbwG/kmbS89uvan/nKK1MufkRahjrJA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8" + } + }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.8", @@ -176,6 +294,41 @@ "System.Text.Json": "9.0.8" } }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "4wGlHsrLhYjLw4sFkfRixu2w4DK7dv60OjbvgbLGhUJk0eUPxYHhnszZ/P18nnAkfrPryvtOJ3ZTVev0kpqM6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "/B8I5bScondnLMNULA3PBu/7Gvmv/P7L83j7gVrmLh6R+HCgHqUNIwVvzCok4ZjIXN2KxrsONHjFYwoBK5EJgQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "zvSjdOAb3HW3aJPM5jf+PR9UoIkoci9id80RXmBgrDEozWI0GDw8tdmpyZgZSwFDvGCwHFodFLNQaeH8879rlA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "System.Text.Json": "9.0.0" + } + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "9.0.8", @@ -207,6 +360,11 @@ "resolved": "9.0.8", "contentHash": "Lj8/a1Hzli1z6jo8H9urc16GxkpVJtJM+W9fmivXMNu7nwzHziGkxn4vO0DFscMbudkEVKSezdDuHk5kgM0X/g==" }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qd01+AqPhbAG14KtdtIqFk+cxHQFZ/oqRSCoxU1F+Q6Kv0cl726sl7RzU9yLFGd4BUOKdN4XojXF0pQf/R6YeA==" + }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "9.0.8", @@ -233,11 +391,13 @@ "Google.Protobuf": "[3.33.0, )", "Grpc.HealthCheck": "[2.71.0, )", "Grpc.Net.ClientFactory": "[2.71.0, )", + "Microsoft.Extensions.Hosting.Abstractions": "[9.0.8, )", "Microsoft.Extensions.Http": "[9.0.8, )", "Microsoft.Extensions.Logging.Console": "[9.0.8, )", + "Microsoft.Extensions.Options": "[9.0.8, )", "System.Linq.Async": "[6.0.3, )" } } } } -} \ No newline at end of file +} diff --git a/src/Weaviate.Client.Tests/packages.lock.json b/src/Weaviate.Client.Tests/packages.lock.json index 0ad1478d..c3e32fc4 100644 --- a/src/Weaviate.Client.Tests/packages.lock.json +++ b/src/Weaviate.Client.Tests/packages.lock.json @@ -165,6 +165,26 @@ "System.Diagnostics.DiagnosticSource": "9.0.8" } }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "4zZbQ4w+hCMm9J+z5NOj3giIPT2MhZxx05HX/MGuAmDBbjOuXlYIIRN+t4V6OLxy5nXZIcXO+dQMB/OWubuDkw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "WNrad20tySNCPe9aJUK7Wfwh+RiyLF+id02FKW8Qfc+HAzNQHazcqMXAbwG/kmbS89uvan/nKK1MufkRahjrJA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8" + } + }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.8", @@ -442,8 +462,10 @@ "Google.Protobuf": "[3.33.0, )", "Grpc.HealthCheck": "[2.71.0, )", "Grpc.Net.ClientFactory": "[2.71.0, )", + "Microsoft.Extensions.Hosting.Abstractions": "[9.0.8, )", "Microsoft.Extensions.Http": "[9.0.8, )", "Microsoft.Extensions.Logging.Console": "[9.0.8, )", + "Microsoft.Extensions.Options": "[9.0.8, )", "System.Linq.Async": "[6.0.3, )" } } diff --git a/src/Weaviate.Client/Auth.cs b/src/Weaviate.Client/Auth.cs new file mode 100644 index 00000000..78b7eb13 --- /dev/null +++ b/src/Weaviate.Client/Auth.cs @@ -0,0 +1,59 @@ +namespace Weaviate.Client; + +public interface ICredentials +{ + internal string GetScopes(); +} + +public static class Auth +{ + public sealed record ApiKeyCredentials(string Value) : ICredentials + { + string ICredentials.GetScopes() => ""; + + public static implicit operator ApiKeyCredentials(string value) => new(value); + } + + public sealed record BearerTokenCredentials( + string AccessToken, + int ExpiresIn = 60, + string RefreshToken = "" + ) : ICredentials + { + string ICredentials.GetScopes() => ""; + } + + public sealed record ClientCredentialsFlow(string ClientSecret, params string?[] Scope) + : ICredentials + { + public string GetScopes() => string.Join(" ", Scope.Where(s => !string.IsNullOrEmpty(s))); + } + + public sealed record ClientPasswordFlow( + string Username, + string Password, + params string?[] Scope + ) : ICredentials + { + public string GetScopes() => string.Join(" ", Scope.Where(s => !string.IsNullOrEmpty(s))); + } + + public static ApiKeyCredentials ApiKey(string value) => new(value); + + public static BearerTokenCredentials BearerToken( + string accessToken, + int expiresIn = 60, + string refreshToken = "" + ) => new(accessToken, expiresIn, refreshToken); + + public static ClientCredentialsFlow ClientCredentials( + string clientSecret, + params string?[] scope + ) => new(clientSecret, scope); + + public static ClientPasswordFlow ClientPassword( + string username, + string password, + params string?[] scope + ) => new(username, password, scope); +} diff --git a/src/Weaviate.Client/ClientConfiguration.cs b/src/Weaviate.Client/ClientConfiguration.cs new file mode 100644 index 00000000..4528460c --- /dev/null +++ b/src/Weaviate.Client/ClientConfiguration.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Logging; + +namespace Weaviate.Client; + +public sealed record ClientConfiguration( + string RestAddress = "localhost", + string RestPath = "v1/", + string GrpcAddress = "localhost", + string GrpcPath = "", + ushort RestPort = 8080, + ushort GrpcPort = 50051, + bool UseSsl = false, + Dictionary? Headers = null, + ICredentials? Credentials = null, + TimeSpan? DefaultTimeout = null, + TimeSpan? InitTimeout = null, + TimeSpan? DataTimeout = null, + TimeSpan? QueryTimeout = null, + RetryPolicy? RetryPolicy = null, + DelegatingHandler[]? CustomHandlers = null, + ITokenServiceFactory? TokenServiceFactory = null +) +{ + public Uri RestUri => + new UriBuilder() + { + Host = RestAddress, + Scheme = UseSsl ? "https" : "http", + Port = RestPort, + Path = RestPath, + }.Uri; + + public Uri GrpcUri => + new UriBuilder() + { + Host = GrpcAddress, + Scheme = UseSsl ? "https" : "http", + Port = GrpcPort, + Path = GrpcPath, + }.Uri; + + /// + /// Builds a WeaviateClient asynchronously, initializing all services in the correct order. + /// This is the recommended way to create clients. + /// + internal async Task BuildAsync(HttpMessageHandler? messageHandler = null) + { + var logger = LoggerFactory + .Create(builder => builder.AddConsole()) + .CreateLogger(); + + // Use factory to create token service + var tokenService = await ( + TokenServiceFactory ?? new DefaultTokenServiceFactory() + ).CreateAsync(this); + + // Create REST client + var restClient = WeaviateClient.CreateRestClient( + this, + messageHandler, + tokenService, + logger + ); + + // Fetch metadata eagerly with init timeout - this will throw if authentication fails + var initTimeout = InitTimeout ?? DefaultTimeout ?? WeaviateDefaults.DefaultTimeout; + var metaCts = new CancellationTokenSource(initTimeout); + var metaDto = await restClient.GetMeta(metaCts.Token); + var meta = new Models.MetaInfo + { + GrpcMaxMessageSize = metaDto?.GrpcMaxMessageSize is not null + ? Convert.ToUInt64(metaDto.GrpcMaxMessageSize) + : null, + Hostname = metaDto?.Hostname ?? string.Empty, + Version = + Models.MetaInfo.ParseWeaviateVersion(metaDto?.Version ?? string.Empty) + ?? new System.Version(0, 0), + Modules = metaDto?.Modules?.ToDictionary() ?? [], + }; + + var maxMessageSize = meta.GrpcMaxMessageSize; + + // Create gRPC client with metadata + var grpcClient = WeaviateClient.CreateGrpcClient(this, tokenService, maxMessageSize); + + // Create and return the client with pre-built services + return new WeaviateClient(this, restClient, grpcClient, logger, meta); + } +}; diff --git a/src/Weaviate.Client/ClusterClient.cs b/src/Weaviate.Client/ClusterClient.cs index ce78ee94..2b0cc472 100644 --- a/src/Weaviate.Client/ClusterClient.cs +++ b/src/Weaviate.Client/ClusterClient.cs @@ -4,11 +4,12 @@ namespace Weaviate.Client; public class ClusterClient { - private readonly Rest.WeaviateRestClient _client; + private readonly WeaviateClient _client; + private Rest.WeaviateRestClient _restClient => _client.RestClient; private NodesClient? _nodes; private ReplicationsClient? _replications; - internal ClusterClient(Rest.WeaviateRestClient client) + internal ClusterClient(WeaviateClient client) { _client = client; } @@ -16,12 +17,12 @@ internal ClusterClient(Rest.WeaviateRestClient client) /// /// Access to cluster nodes management /// - public NodesClient Nodes => _nodes ??= new NodesClient(_client); + public NodesClient Nodes => _nodes ??= new NodesClient(_restClient); /// /// Access to replication operations management /// - public ReplicationsClient Replications => _replications ??= new ReplicationsClient(_client); + public ReplicationsClient Replications => _replications ??= new ReplicationsClient(_restClient); /// /// Start a replication operation asynchronously. @@ -47,11 +48,11 @@ public async Task Replicate( : Rest.Dto.ReplicationReplicateReplicaRequestType.COPY, }; - var response = await _client.ReplicateAsync(dto); + var response = await _restClient.ReplicateAsync(dto); var operationId = response.Id; // Fetch initial status - var initialDetails = await _client.ReplicationDetailsAsync( + var initialDetails = await _restClient.ReplicationDetailsAsync( operationId, cancellationToken: cancellationToken ); @@ -68,7 +69,7 @@ public async Task Replicate( initialOperation, async (ct) => { - var details = await _client.ReplicationDetailsAsync( + var details = await _restClient.ReplicationDetailsAsync( operationId, cancellationToken: ct == default ? cancellationToken : ct ); @@ -80,7 +81,7 @@ public async Task Replicate( : ReplicationsClient.ToModel(details); }, async (ct) => - await _client.CancelReplicationAsync( + await _restClient.CancelReplicationAsync( operationId, ct == default ? cancellationToken : ct ) diff --git a/src/Weaviate.Client/CollectionsClient.cs b/src/Weaviate.Client/CollectionsClient.cs index 7fdc7028..3f6eb366 100644 --- a/src/Weaviate.Client/CollectionsClient.cs +++ b/src/Weaviate.Client/CollectionsClient.cs @@ -104,14 +104,23 @@ public CollectionClient Use(string name) /// /// The C# type representing objects in this collection. /// The name of the collection. - /// If true, validates that type T is compatible with the collection schema on construction. Default is false for performance. /// A TypedCollectionClient that provides strongly-typed operations. - /// Thrown if validateType is true and validation fails with errors. - public async Task> Use(string name, bool validateType = false) + public Typed.TypedCollectionClient Use(string name) where T : class, new() { - var innerClient = Use(name); + return Use(name).AsTyped(); + } - return await innerClient.AsTyped(validateType); + /// + /// Creates a strongly-typed collection client for accessing a specific collection. + /// The collection client provides type-safe data and query operations. + /// + /// The C# type representing objects in this collection. + /// The name of the collection. + /// A TypedCollectionClient that provides strongly-typed operations. + public async Task> Use(string name, bool validateType) + where T : class, new() + { + return await Use(name).AsTyped(validateType: validateType); } } diff --git a/src/Weaviate.Client/DefaultTokenServiceFactory.cs b/src/Weaviate.Client/DefaultTokenServiceFactory.cs new file mode 100644 index 00000000..68538420 --- /dev/null +++ b/src/Weaviate.Client/DefaultTokenServiceFactory.cs @@ -0,0 +1,136 @@ +using System.Net.Http; + +namespace Weaviate.Client; + +public class DefaultTokenServiceFactory : ITokenServiceFactory +{ + public async Task CreateAsync(ClientConfiguration configuration) + { + if (configuration.Credentials is null) + return null; + + if (configuration.Credentials is Auth.ApiKeyCredentials apiKey) + return new ApiKeyTokenService(apiKey); + + var openIdConfig = await OAuthTokenService.GetOpenIdConfig( + configuration.RestUri.ToString() + ); + if (!openIdConfig.IsSuccessStatusCode) + return null; + + var tokenEndpoint = openIdConfig.TokenEndpoint!; + var clientId = openIdConfig.ClientID!; + + OAuthConfig oauthConfig = new() + { + TokenEndpoint = tokenEndpoint, + ClientId = clientId, + GrantType = configuration.Credentials switch + { + Auth.ClientCredentialsFlow => "client_credentials", + Auth.ClientPasswordFlow => "password", + Auth.BearerTokenCredentials => "bearer", + _ => throw new NotSupportedException("Unsupported credentials type"), + }, + Scope = configuration.Credentials?.GetScopes() ?? "", + }; + + var httpClient = new HttpClient(); + + if (configuration.Credentials is Auth.BearerTokenCredentials bearerToken) + { + return new OAuthTokenService(httpClient, oauthConfig) + { + CurrentToken = new( + bearerToken.AccessToken, + bearerToken.ExpiresIn, + bearerToken.RefreshToken + ), + }; + } + + if (configuration.Credentials is Auth.ClientCredentialsFlow clientCreds) + { + oauthConfig = oauthConfig with { ClientSecret = clientCreds.ClientSecret }; + } + else if (configuration.Credentials is Auth.ClientPasswordFlow clientPass) + { + oauthConfig = oauthConfig with + { + Username = clientPass.Username, + Password = clientPass.Password, + }; + } + + return new OAuthTokenService(httpClient, oauthConfig); + } + + public ITokenService? CreateSync(ClientConfiguration configuration) + { + if (configuration.Credentials is null) + return null; + + if (configuration.Credentials is Auth.ApiKeyCredentials apiKey) + return new ApiKeyTokenService(apiKey); + + try + { + var openIdConfig = OAuthTokenService + .GetOpenIdConfig(configuration.RestUri.ToString()) + .GetAwaiter() + .GetResult(); + if (!openIdConfig.IsSuccessStatusCode) + return null; + + var tokenEndpoint = openIdConfig.TokenEndpoint!; + var clientId = openIdConfig.ClientID!; + + OAuthConfig oauthConfig = new() + { + TokenEndpoint = tokenEndpoint, + ClientId = clientId, + GrantType = configuration.Credentials switch + { + Auth.ClientCredentialsFlow => "client_credentials", + Auth.ClientPasswordFlow => "password", + Auth.BearerTokenCredentials => "bearer", + _ => throw new NotSupportedException("Unsupported credentials type"), + }, + Scope = configuration.Credentials?.GetScopes() ?? "", + }; + + var httpClient = new HttpClient(); + + if (configuration.Credentials is Auth.BearerTokenCredentials bearerToken) + { + return new OAuthTokenService(httpClient, oauthConfig) + { + CurrentToken = new( + bearerToken.AccessToken, + bearerToken.ExpiresIn, + bearerToken.RefreshToken + ), + }; + } + + if (configuration.Credentials is Auth.ClientCredentialsFlow clientCreds) + { + oauthConfig = oauthConfig with { ClientSecret = clientCreds.ClientSecret }; + } + else if (configuration.Credentials is Auth.ClientPasswordFlow clientPass) + { + oauthConfig = oauthConfig with + { + Username = clientPass.Username, + Password = clientPass.Password, + }; + } + + return new OAuthTokenService(httpClient, oauthConfig); + } + catch + { + return null; + } + } +} diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs b/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs index 3157ed16..76508766 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs @@ -17,7 +17,8 @@ internal class WeaviateClientFactory : IWeaviateClientFactory, IDisposable public WeaviateClientFactory( IOptionsMonitor optionsMonitor, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory + ) { _optionsMonitor = optionsMonitor; _loggerFactory = loggerFactory; @@ -33,8 +34,10 @@ public WeaviateClient GetClient(string name) if (_disposed) throw new ObjectDisposedException(nameof(WeaviateClientFactory)); - var lazyClient = _clients.GetOrAdd(name, n => new Lazy>( - () => CreateClientAsync(n))); + var lazyClient = _clients.GetOrAdd( + name, + n => new Lazy>(() => CreateClientAsync(n)) + ); // This will block if the client is still initializing // For non-blocking behavior, use GetClientAsync() @@ -45,13 +48,18 @@ public WeaviateClient GetClient(string name) /// Gets or creates a client asynchronously. /// Ensures the client is fully initialized before returning. /// - public async Task GetClientAsync(string name, CancellationToken cancellationToken = default) + public async Task GetClientAsync( + string name, + CancellationToken cancellationToken = default + ) { if (_disposed) throw new ObjectDisposedException(nameof(WeaviateClientFactory)); - var lazyClient = _clients.GetOrAdd(name, n => new Lazy>( - () => CreateClientAsync(n))); + var lazyClient = _clients.GetOrAdd( + name, + n => new Lazy>(() => CreateClientAsync(n)) + ); return await lazyClient.Value; } @@ -62,7 +70,7 @@ private async Task CreateClientAsync(string name) var logger = _loggerFactory.CreateLogger(); var clientOptions = Options.Create(options); - var client = new WeaviateClient(clientOptions, logger); + var client = new WeaviateClient(clientOptions, null, logger); // Initialize the client await client.InitializeAsync(); diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs index 86b93e30..1f74dad9 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs @@ -31,7 +31,8 @@ public static IServiceCollection AddWeaviateClientFactory(this IServiceCollectio public static IServiceCollection AddWeaviateClient( this IServiceCollection services, string name, - Action configureOptions) + Action configureOptions + ) { // Ensure factory is registered if (!services.Any(x => x.ServiceType == typeof(IWeaviateClientFactory))) @@ -56,7 +57,8 @@ public static IServiceCollection AddWeaviateClient( public static IServiceCollection AddWeaviate( this IServiceCollection services, Action configureOptions, - bool eagerInitialization = true) + bool eagerInitialization = true + ) { services.Configure(configureOptions); services.AddSingleton(); @@ -79,7 +81,8 @@ public static IServiceCollection AddWeaviate( public static IServiceCollection AddWeaviate( this IServiceCollection services, IConfiguration configuration, - bool eagerInitialization = true) + bool eagerInitialization = true + ) { services.Configure(configuration); services.AddSingleton(); @@ -121,22 +124,26 @@ public static IServiceCollection AddWeaviateLocal( TimeSpan? initTimeout = null, TimeSpan? dataTimeout = null, TimeSpan? queryTimeout = null, - bool eagerInitialization = true) + bool eagerInitialization = true + ) { - services.AddWeaviate(options => - { - options.RestEndpoint = hostname; - options.GrpcEndpoint = hostname; - options.RestPort = restPort; - options.GrpcPort = grpcPort; - options.UseSsl = useSsl; - options.Credentials = credentials; - options.Headers = headers; - options.DefaultTimeout = defaultTimeout; - options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; - options.QueryTimeout = queryTimeout; - }, eagerInitialization); + services.AddWeaviate( + options => + { + options.RestEndpoint = hostname; + options.GrpcEndpoint = hostname; + options.RestPort = restPort; + options.GrpcPort = grpcPort; + options.UseSsl = useSsl; + options.Credentials = credentials; + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; + }, + eagerInitialization + ); return services; } @@ -164,22 +171,26 @@ public static IServiceCollection AddWeaviateCloud( TimeSpan? initTimeout = null, TimeSpan? dataTimeout = null, TimeSpan? queryTimeout = null, - bool eagerInitialization = true) + bool eagerInitialization = true + ) { - services.AddWeaviate(options => - { - options.RestEndpoint = clusterEndpoint; - options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; - options.RestPort = 443; - options.GrpcPort = 443; - options.UseSsl = true; - options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); - options.Headers = headers; - options.DefaultTimeout = defaultTimeout; - options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; - options.QueryTimeout = queryTimeout; - }, eagerInitialization); + services.AddWeaviate( + options => + { + options.RestEndpoint = clusterEndpoint; + options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; + }, + eagerInitialization + ); return services; } @@ -193,18 +204,19 @@ public static IServiceCollection AddWeaviateCloud( /// The service collection. /// The logical name of the client. /// The service collection for method chaining. - public static IServiceCollection AddWeaviateLocal( - this IServiceCollection services, - string name) + public static IServiceCollection AddWeaviateLocal(this IServiceCollection services, string name) { - return services.AddWeaviateClient(name, options => - { - options.RestEndpoint = "localhost"; - options.GrpcEndpoint = "localhost"; - options.RestPort = 8080; - options.GrpcPort = 50051; - options.UseSsl = false; - }); + return services.AddWeaviateClient( + name, + options => + { + options.RestEndpoint = "localhost"; + options.GrpcEndpoint = "localhost"; + options.RestPort = 8080; + options.GrpcPort = 50051; + options.UseSsl = false; + } + ); } /// @@ -217,7 +229,8 @@ public static IServiceCollection AddWeaviateLocal( public static IServiceCollection AddWeaviateLocal( this IServiceCollection services, string name, - Action configureOptions) + Action configureOptions + ) { return services.AddWeaviateClient(name, configureOptions); } @@ -250,22 +263,26 @@ public static IServiceCollection AddWeaviateLocal( TimeSpan? defaultTimeout = null, TimeSpan? initTimeout = null, TimeSpan? dataTimeout = null, - TimeSpan? queryTimeout = null) + TimeSpan? queryTimeout = null + ) { - return services.AddWeaviateClient(name, options => - { - options.RestEndpoint = hostname; - options.GrpcEndpoint = hostname; - options.RestPort = restPort; - options.GrpcPort = grpcPort; - options.UseSsl = useSsl; - options.Credentials = credentials; - options.Headers = headers; - options.DefaultTimeout = defaultTimeout; - options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; - options.QueryTimeout = queryTimeout; - }); + return services.AddWeaviateClient( + name, + options => + { + options.RestEndpoint = hostname; + options.GrpcEndpoint = hostname; + options.RestPort = restPort; + options.GrpcPort = grpcPort; + options.UseSsl = useSsl; + options.Credentials = credentials; + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; + } + ); } /// @@ -278,7 +295,8 @@ public static IServiceCollection AddWeaviateLocal( public static IServiceCollection AddWeaviateCloud( this IServiceCollection services, string name, - Action configureOptions) + Action configureOptions + ) { return services.AddWeaviateClient(name, configureOptions); } @@ -305,79 +323,25 @@ public static IServiceCollection AddWeaviateCloud( TimeSpan? defaultTimeout = null, TimeSpan? initTimeout = null, TimeSpan? dataTimeout = null, - TimeSpan? queryTimeout = null) - { - return services.AddWeaviateClient(name, options => - { - options.RestEndpoint = clusterEndpoint; - options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; - options.RestPort = 443; - options.GrpcPort = 443; - options.UseSsl = true; - options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); - options.Headers = headers; - options.DefaultTimeout = defaultTimeout; - options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; - options.QueryTimeout = queryTimeout; - }); - } - - /// - /// Adds a named Weaviate client configured for a local instance. - /// - /// The service collection. - /// The logical name of the client. - /// Hostname for local Weaviate instance. Default is "localhost". - /// REST port. Default is 8080. - /// gRPC port. Default is 50051. - /// Whether to use SSL/TLS. Default is false. - /// Authentication credentials. - /// The service collection for method chaining. - [Obsolete("Use AddWeaviateLocal(name, hostname, ...) instead for better API consistency.")] - public static IServiceCollection AddWeaviateClient( - this IServiceCollection services, - string name, - string hostname = "localhost", - ushort restPort = 8080, - ushort grpcPort = 50051, - bool useSsl = false, - ICredentials? credentials = null) + TimeSpan? queryTimeout = null + ) { - return services.AddWeaviateClient(name, options => - { - options.RestEndpoint = hostname; - options.GrpcEndpoint = hostname; - options.RestPort = restPort; - options.GrpcPort = grpcPort; - options.UseSsl = useSsl; - options.Credentials = credentials; - }); - } - - /// - /// Adds a named Weaviate client configured for Weaviate Cloud. - /// - /// The service collection. - /// The logical name of the client. - /// The Weaviate Cloud cluster endpoint (e.g., "my-cluster.weaviate.cloud"). - /// API key for authentication. - /// The service collection for method chaining. - [Obsolete("Use AddWeaviateCloud(name, clusterEndpoint, apiKey) instead for better API consistency.")] - public static IServiceCollection AddWeaviateCloudClient( - this IServiceCollection services, - string name, - string clusterEndpoint, - string? apiKey = null) - { - return services.AddWeaviateClient(name, options => - { - options.RestEndpoint = clusterEndpoint; - options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; - options.RestPort = 443; - options.GrpcPort = 443; - options.UseSsl = true; - options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); - }); + return services.AddWeaviateClient( + name, + options => + { + options.RestEndpoint = clusterEndpoint; + options.GrpcEndpoint = $"grpc-{clusterEndpoint}"; + options.RestPort = 443; + options.GrpcPort = 443; + options.UseSsl = true; + options.Credentials = string.IsNullOrEmpty(apiKey) ? null : Auth.ApiKey(apiKey); + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.DataTimeout = dataTimeout; + options.QueryTimeout = queryTimeout; + } + ); } } diff --git a/src/Weaviate.Client/ITokenServiceFactory.cs b/src/Weaviate.Client/ITokenServiceFactory.cs new file mode 100644 index 00000000..f2c131ae --- /dev/null +++ b/src/Weaviate.Client/ITokenServiceFactory.cs @@ -0,0 +1,7 @@ +namespace Weaviate.Client; + +public interface ITokenServiceFactory +{ + Task CreateAsync(ClientConfiguration configuration); + ITokenService? CreateSync(ClientConfiguration configuration); +} diff --git a/src/Weaviate.Client/Typed/TypedCollectionClient.cs b/src/Weaviate.Client/Typed/TypedCollectionClient.cs index 5324b6de..b5ae132c 100644 --- a/src/Weaviate.Client/Typed/TypedCollectionClient.cs +++ b/src/Weaviate.Client/Typed/TypedCollectionClient.cs @@ -163,24 +163,4 @@ var obj in _collectionClient.Iterator( yield return obj.ToTyped(); } } - - /// - /// Validates that the C# type T is compatible with this collection's schema. - /// Checks property names, types, and array handling. - /// - /// Optional type validator instance. If null, uses TypeValidator.Default. - /// Optional schema cache instance. If null, uses SchemaCache.Default. - /// Cancellation token. - /// A validation result containing any errors and warnings. - /// Thrown if the collection schema cannot be fetched. - public async Task ValidateType( - TypeValidator? typeValidator = null, - SchemaCache? schemaCache = null, - CancellationToken cancellationToken = default - ) - { - var schema = await Config.GetCachedConfig(schemaCache, cancellationToken); - - return schema.ValidateType(typeValidator); - } } diff --git a/src/Weaviate.Client/Validation/ValidationExtensions.cs b/src/Weaviate.Client/Validation/ValidationExtensions.cs index b4543527..5b59c1b7 100644 --- a/src/Weaviate.Client/Validation/ValidationExtensions.cs +++ b/src/Weaviate.Client/Validation/ValidationExtensions.cs @@ -1,4 +1,7 @@ -namespace Weaviate.Client.Validation; +using Weaviate.Client.Cache; +using Weaviate.Client.Validation; + +namespace Weaviate.Client; /// /// Extension methods for type validation operations. @@ -51,4 +54,48 @@ public static ValidationResult ValidateType( var validator = typeValidator ?? TypeValidator.Default; return validator.ValidateType(schema); } + + /// + /// Validates a C# type against a collection schema. + /// + /// The C# type to validate. + /// Optional type validator instance. If null, uses TypeValidator.Default. + /// Optional schema cache instance. If null, uses SchemaCache.Default. + /// Cancellation token. + /// A validation result containing any errors and warnings. + public static async Task ValidateType( + this Typed.TypedCollectionClient client, + TypeValidator? typeValidator = null, + SchemaCache? schemaCache = null, + CancellationToken cancellationToken = default + ) + where T : class, new() + { + var schema = await client.Config.GetCachedConfig(schemaCache, cancellationToken); + + return schema.ValidateType(typeValidator); + } + + /// + /// Validates a C# type against a collection schema and throws if validation fails. + /// + /// The C# type to validate. + /// The collection schema to validate against. + /// The collection name (for error messages). + /// Optional type validator instance. If null, uses TypeValidator.Default. + /// Thrown if validation fails with errors. + public static async Task> ValidateTypeOrThrow( + this Typed.TypedCollectionClient client, + TypeValidator? typeValidator = null, + SchemaCache? schemaCache = null, + CancellationToken cancellationToken = default + ) + where T : class, new() + { + var schema = await client.Config.GetCachedConfig(schemaCache, cancellationToken); + + schema.ValidateTypeOrThrow(typeValidator); + + return client; + } } diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index cd7d33ee..ff0b8e06 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -8,336 +8,6 @@ namespace Weaviate.Client; -/// -/// Global default settings for Weaviate clients. -/// -public static class WeaviateDefaults -{ - /// - /// Default timeout for all requests. Default is 30 seconds. - /// This can be overridden per client via ClientConfiguration. - /// - public static TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(30); - - /// - /// Default timeout for initialization operations (GetMeta, Live, IsReady). Default is 2 seconds. - /// This can be overridden per client via ClientConfiguration.WithInitTimeout(). - /// - public static TimeSpan InitTimeout { get; set; } = TimeSpan.FromSeconds(2); - - /// - /// Default timeout for data operations (Insert, Delete, Update, Reference management). Default is 120 seconds. - /// This can be overridden per client via ClientConfiguration.WithInsertTimeout(). - /// - public static TimeSpan InsertTimeout { get; set; } = TimeSpan.FromSeconds(120); - - /// - /// Default timeout for query/search operations (FetchObjects, NearText, BM25, Hybrid, etc.). Default is 60 seconds. - /// This can be overridden per client via ClientConfiguration.WithQueryTimeout(). - /// - public static TimeSpan QueryTimeout { get; set; } = TimeSpan.FromSeconds(60); - - /// - /// Default retry policy applied when a client does not specify one explicitly. - /// - public static RetryPolicy DefaultRetryPolicy { get; set; } = RetryPolicy.Default; -} - -public interface ICredentials -{ - internal string GetScopes(); -} - -public static class Auth -{ - public sealed record ApiKeyCredentials(string Value) : ICredentials - { - string ICredentials.GetScopes() => ""; - - public static implicit operator ApiKeyCredentials(string value) => new(value); - } - - public sealed record BearerTokenCredentials( - string AccessToken, - int ExpiresIn = 60, - string RefreshToken = "" - ) : ICredentials - { - string ICredentials.GetScopes() => ""; - } - - public sealed record ClientCredentialsFlow(string ClientSecret, params string?[] Scope) - : ICredentials - { - public string GetScopes() => string.Join(" ", Scope.Where(s => !string.IsNullOrEmpty(s))); - } - - public sealed record ClientPasswordFlow( - string Username, - string Password, - params string?[] Scope - ) : ICredentials - { - public string GetScopes() => string.Join(" ", Scope.Where(s => !string.IsNullOrEmpty(s))); - } - - public static ApiKeyCredentials ApiKey(string value) => new(value); - - public static BearerTokenCredentials BearerToken( - string accessToken, - int expiresIn = 60, - string refreshToken = "" - ) => new(accessToken, expiresIn, refreshToken); - - public static ClientCredentialsFlow ClientCredentials( - string clientSecret, - params string?[] scope - ) => new(clientSecret, scope); - - public static ClientPasswordFlow ClientPassword( - string username, - string password, - params string?[] scope - ) => new(username, password, scope); -} - -public sealed record ClientConfiguration( - string RestAddress = "localhost", - string RestPath = "v1/", - string GrpcAddress = "localhost", - string GrpcPath = "", - ushort RestPort = 8080, - ushort GrpcPort = 50051, - bool UseSsl = false, - Dictionary? Headers = null, - ICredentials? Credentials = null, - TimeSpan? DefaultTimeout = null, - TimeSpan? InitTimeout = null, - TimeSpan? InsertTimeout = null, - TimeSpan? QueryTimeout = null, - RetryPolicy? RetryPolicy = null, - DelegatingHandler[]? CustomHandlers = null -) -{ - public Uri RestUri => - new UriBuilder() - { - Host = RestAddress, - Scheme = UseSsl ? "https" : "http", - Port = RestPort, - Path = RestPath, - }.Uri; - - public Uri GrpcUri => - new UriBuilder() - { - Host = GrpcAddress, - Scheme = UseSsl ? "https" : "http", - Port = GrpcPort, - Path = GrpcPath, - }.Uri; - - /// - /// Builds a WeaviateClient asynchronously, initializing all services in the correct order. - /// This is the recommended way to create clients. - /// - internal async Task BuildAsync(HttpMessageHandler? messageHandler = null) - { - var logger = LoggerFactory - .Create(builder => builder.AddConsole()) - .CreateLogger(); - - // Initialize token service asynchronously - var tokenService = await InitializeTokenService(this); - - // Create REST client - var restClient = WeaviateClient.CreateRestClient( - this, - messageHandler, - tokenService, - logger - ); - - // Fetch metadata eagerly with init timeout - this will throw if authentication fails - var initTimeout = InitTimeout ?? DefaultTimeout ?? WeaviateDefaults.DefaultTimeout; - var metaCts = new CancellationTokenSource(initTimeout); - var metaDto = await restClient.GetMeta(metaCts.Token); - var meta = new Models.MetaInfo - { - GrpcMaxMessageSize = metaDto?.GrpcMaxMessageSize is not null - ? Convert.ToUInt64(metaDto.GrpcMaxMessageSize) - : null, - Hostname = metaDto?.Hostname ?? string.Empty, - Version = - Models.MetaInfo.ParseWeaviateVersion(metaDto?.Version ?? string.Empty) - ?? new System.Version(0, 0), - Modules = metaDto?.Modules?.ToDictionary() ?? [], - }; - - var maxMessageSize = meta.GrpcMaxMessageSize; - - // Create gRPC client with metadata - var grpcClient = WeaviateClient.CreateGrpcClient(this, tokenService, maxMessageSize); - - // Create and return the client with pre-built services - return new WeaviateClient(this, restClient, grpcClient, logger, meta); - } - - /// - /// Asynchronous token service initialization. - /// - private static async Task InitializeTokenService( - ClientConfiguration configuration - ) - { - if (configuration.Credentials is null) - { - return null; - } - - if (configuration.Credentials is Auth.ApiKeyCredentials apiKey) - { - return new ApiKeyTokenService(apiKey); - } - - var openIdConfig = await OAuthTokenService.GetOpenIdConfig( - configuration.RestUri.ToString() - ); - - if (!openIdConfig.IsSuccessStatusCode) - { - return null; - } - - var tokenEndpoint = openIdConfig.TokenEndpoint!; - var clientId = openIdConfig.ClientID!; - - OAuthConfig oauthConfig = new() - { - TokenEndpoint = tokenEndpoint, - ClientId = clientId, - GrantType = configuration.Credentials switch - { - Auth.ClientCredentialsFlow => "client_credentials", - Auth.ClientPasswordFlow => "password", - Auth.BearerTokenCredentials => "bearer", - _ => throw new NotSupportedException("Unsupported credentials type"), - }, - Scope = configuration.Credentials?.GetScopes() ?? "", - }; - - var httpClient = new HttpClient(); - - if (configuration.Credentials is Auth.BearerTokenCredentials bearerToken) - { - return new OAuthTokenService(httpClient, oauthConfig) - { - CurrentToken = new( - bearerToken.AccessToken, - bearerToken.ExpiresIn, - bearerToken.RefreshToken - ), - }; - } - - if (configuration.Credentials is Auth.ClientCredentialsFlow clientCreds) - { - oauthConfig = oauthConfig with { ClientSecret = clientCreds.ClientSecret }; - } - else if (configuration.Credentials is Auth.ClientPasswordFlow clientPass) - { - oauthConfig = oauthConfig with - { - Username = clientPass.Username, - Password = clientPass.Password, - }; - } - - return new OAuthTokenService(httpClient, oauthConfig); - } - - /// - /// Synchronous wrapper for InitializeTokenService for use in the builder pattern. - /// - internal static ITokenService? InitializeTokenServiceSync(ClientConfiguration configuration) - { - if (configuration.Credentials is null) - { - return null; - } - - if (configuration.Credentials is Auth.ApiKeyCredentials apiKey) - { - return new ApiKeyTokenService(apiKey); - } - - // For OAuth credentials, we need to fetch the OpenID config synchronously - // This is acceptable here since it's only during initial construction - try - { - var openIdConfig = OAuthTokenService - .GetOpenIdConfig(configuration.RestUri.ToString()) - .GetAwaiter() - .GetResult(); - - if (!openIdConfig.IsSuccessStatusCode) - { - return null; - } - - var tokenEndpoint = openIdConfig.TokenEndpoint!; - var clientId = openIdConfig.ClientID!; - - OAuthConfig oauthConfig = new() - { - TokenEndpoint = tokenEndpoint, - ClientId = clientId, - GrantType = configuration.Credentials switch - { - Auth.ClientCredentialsFlow => "client_credentials", - Auth.ClientPasswordFlow => "password", - Auth.BearerTokenCredentials => "bearer", - _ => throw new NotSupportedException("Unsupported credentials type"), - }, - Scope = configuration.Credentials?.GetScopes() ?? "", - }; - - var httpClient = new HttpClient(); - - if (configuration.Credentials is Auth.BearerTokenCredentials bearerToken) - { - return new OAuthTokenService(httpClient, oauthConfig) - { - CurrentToken = new( - bearerToken.AccessToken, - bearerToken.ExpiresIn, - bearerToken.RefreshToken - ), - }; - } - - if (configuration.Credentials is Auth.ClientCredentialsFlow clientCreds) - { - oauthConfig = oauthConfig with { ClientSecret = clientCreds.ClientSecret }; - } - else if (configuration.Credentials is Auth.ClientPasswordFlow clientPass) - { - oauthConfig = oauthConfig with - { - Username = clientPass.Username, - Password = clientPass.Password, - }; - } - - return new OAuthTokenService(httpClient, oauthConfig); - } - catch - { - return null; - } - } -}; - public partial class WeaviateClient : IDisposable { private static readonly Lazy _defaultOptions = new(() => @@ -351,6 +21,7 @@ public partial class WeaviateClient : IDisposable // Async initialization support private readonly Lazy? _initializationTask; + private readonly ITokenServiceFactory? _tokenServiceFactory; private readonly ClientConfiguration? _configForAsyncInit; public async Task GetMeta(CancellationToken cancellationToken = default) @@ -457,7 +128,6 @@ private CancellationToken CreateInitCancellationToken(CancellationToken userToke public CollectionsClient Collections { get; } public ClusterClient Cluster { get; } - public AliasClient Alias { get; } public UsersClient Users { get; } public RolesClient Roles { get; } @@ -472,7 +142,7 @@ static bool IsWeaviateDomain(string url) public TimeSpan? DefaultTimeout => Configuration.DefaultTimeout; public TimeSpan? InitTimeout => Configuration.InitTimeout; - public TimeSpan? InsertTimeout => Configuration.InsertTimeout; + public TimeSpan? DataTimeout => Configuration.DataTimeout; public TimeSpan? QueryTimeout => Configuration.QueryTimeout; /// @@ -493,7 +163,7 @@ internal WeaviateClient( GrpcClient = grpcClient; _metaCache = meta; - Cluster = new ClusterClient(RestClient); + Cluster = new ClusterClient(this); Collections = new CollectionsClient(this); Alias = new AliasClient(this); Users = new UsersClient(this); @@ -508,16 +178,25 @@ internal WeaviateClient( public WeaviateClient( ClientConfiguration? configuration = null, HttpMessageHandler? httpMessageHandler = null, + ITokenServiceFactory? tokenServiceFactory = null, ILogger? logger = null ) { var config = configuration ?? DefaultOptions; + + var tokenService = tokenServiceFactory?.CreateSync(config); + var loggerInstance = logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - var restClient = CreateRestClientForPublic(config, httpMessageHandler, loggerInstance); - var grpcClientInstance = CreateGrpcClientForPublic(config); + var restClient = CreateRestClientForPublic( + config, + httpMessageHandler, + tokenService, + loggerInstance + ); + var grpcClientInstance = CreateGrpcClientForPublic(config, tokenService); // Initialize like the internal constructor _logger = loggerInstance; @@ -525,7 +204,7 @@ public WeaviateClient( RestClient = restClient; GrpcClient = grpcClientInstance; - Cluster = new ClusterClient(RestClient); + Cluster = new ClusterClient(this); Collections = new CollectionsClient(this); Alias = new AliasClient(this); Users = new UsersClient(this); @@ -539,6 +218,7 @@ public WeaviateClient( internal WeaviateClient( ClientConfiguration? configuration = null, HttpMessageHandler? httpMessageHandler = null, + ITokenService? tokenService = null, ILogger? logger = null, WeaviateGrpcClient? grpcClient = null ) @@ -548,8 +228,13 @@ internal WeaviateClient( logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - var restClient = CreateRestClientForPublic(config, httpMessageHandler, loggerInstance); - var grpcClientInstance = grpcClient ?? CreateGrpcClientForPublic(config); + var restClient = CreateRestClientForPublic( + config, + httpMessageHandler, + tokenService, + loggerInstance + ); + var grpcClientInstance = grpcClient ?? CreateGrpcClientForPublic(config, tokenService); // Initialize like the internal constructor _logger = loggerInstance; @@ -557,7 +242,7 @@ internal WeaviateClient( RestClient = restClient; GrpcClient = grpcClientInstance; - Cluster = new ClusterClient(RestClient); + Cluster = new ClusterClient(this); Collections = new CollectionsClient(this); Alias = new AliasClient(this); Users = new UsersClient(this); @@ -570,9 +255,7 @@ internal WeaviateClient( /// Uses async initialization pattern - call InitializeAsync() or ensure IHostedService runs. /// public WeaviateClient(IOptions options) - : this(options, null) - { - } + : this(options, null, null) { } /// /// Constructor for dependency injection scenarios with logger. @@ -580,19 +263,24 @@ public WeaviateClient(IOptions options) /// public WeaviateClient( IOptions options, - ILogger? logger) + ITokenServiceFactory? tokenServiceFactory, + ILogger? logger + ) { var weaviateOptions = options.Value; _configForAsyncInit = weaviateOptions.ToClientConfiguration(); Configuration = _configForAsyncInit; - _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + _logger = + logger + ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + _tokenServiceFactory = tokenServiceFactory; // Initialize Lazy task that will run initialization on first access _initializationTask = new Lazy(() => PerformInitializationAsync(_configForAsyncInit)); - // Initialize sub-clients - they will be properly set up after async initialization Collections = new CollectionsClient(this); - Cluster = new ClusterClient(null!); // Will be set after init + Cluster = new ClusterClient(this); Alias = new AliasClient(this); Users = new UsersClient(this); Roles = new RolesClient(this); @@ -608,13 +296,16 @@ private async Task PerformInitializationAsync(ClientConfiguration config) _logger.LogDebug("Starting Weaviate client initialization..."); // Initialize token service asynchronously - var tokenService = await ClientConfiguration.InitializeTokenService(config); + var tokenService = await ( + _tokenServiceFactory ?? new DefaultTokenServiceFactory() + ).CreateAsync(config); // Create REST client RestClient = CreateRestClient(config, null, tokenService, _logger); // Fetch metadata eagerly with init timeout - this will throw if authentication fails - var initTimeout = config.InitTimeout ?? config.DefaultTimeout ?? WeaviateDefaults.DefaultTimeout; + var initTimeout = + config.InitTimeout ?? config.DefaultTimeout ?? WeaviateDefaults.DefaultTimeout; var metaCts = new CancellationTokenSource(initTimeout); var metaDto = await RestClient.GetMeta(metaCts.Token); _metaCache = new Models.MetaInfo @@ -629,14 +320,11 @@ private async Task PerformInitializationAsync(ClientConfiguration config) Modules = metaDto?.Modules?.ToDictionary() ?? [], }; - var maxMessageSize = _metaCache.GrpcMaxMessageSize; + var maxMessageSize = _metaCache?.GrpcMaxMessageSize; // Create gRPC client with metadata GrpcClient = CreateGrpcClient(config, tokenService, maxMessageSize); - // Update Cluster client with the REST client - Cluster = new ClusterClient(RestClient); - _logger.LogDebug("Weaviate client initialization completed"); } @@ -663,7 +351,8 @@ public Task InitializeAsync(CancellationToken cancellationToken = default) /// Checks if the client is fully initialized. /// public bool IsInitialized => - _initializationTask == null || // Non-DI constructor, always ready + _initializationTask == null + || // Non-DI constructor, always ready (_initializationTask.IsValueCreated && _initializationTask.Value.IsCompletedSuccessfully); /// @@ -684,6 +373,7 @@ private async Task EnsureInitializedAsync() private static WeaviateRestClient CreateRestClientForPublic( ClientConfiguration config, HttpMessageHandler? httpMessageHandler, + ITokenService? tokenService, ILogger? logger ) { @@ -691,16 +381,17 @@ private static WeaviateRestClient CreateRestClientForPublic( logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - var tokenService = ClientConfiguration.InitializeTokenServiceSync(config); return CreateRestClient(config, httpMessageHandler, tokenService, loggerInstance); } /// /// Helper to create gRPC client for the public constructor. /// - private static WeaviateGrpcClient CreateGrpcClientForPublic(ClientConfiguration config) + private static WeaviateGrpcClient CreateGrpcClientForPublic( + ClientConfiguration config, + ITokenService? tokenService + ) { - var tokenService = ClientConfiguration.InitializeTokenServiceSync(config); return CreateGrpcClient(config, tokenService); } diff --git a/src/Weaviate.Client/WeaviateDefaults.cs b/src/Weaviate.Client/WeaviateDefaults.cs new file mode 100644 index 00000000..a491e9f7 --- /dev/null +++ b/src/Weaviate.Client/WeaviateDefaults.cs @@ -0,0 +1,36 @@ +namespace Weaviate.Client; + +/// +/// Global default settings for Weaviate clients. +/// +public static class WeaviateDefaults +{ + /// + /// Default timeout for all requests. Default is 30 seconds. + /// This can be overridden per client via ClientConfiguration. + /// + public static TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Default timeout for initialization operations (GetMeta, Live, IsReady). Default is 2 seconds. + /// This can be overridden per client via ClientConfiguration.WithInitTimeout(). + /// + public static TimeSpan InitTimeout { get; set; } = TimeSpan.FromSeconds(2); + + /// + /// Default timeout for data operations (Insert, Delete, Update, Reference management). Default is 120 seconds. + /// This can be overridden per client via ClientConfiguration.WithDataTimeout(). + /// + public static TimeSpan DataTimeout { get; set; } = TimeSpan.FromSeconds(120); + + /// + /// Default timeout for query/search operations (FetchObjects, NearText, BM25, Hybrid, etc.). Default is 60 seconds. + /// This can be overridden per client via ClientConfiguration.WithQueryTimeout(). + /// + public static TimeSpan QueryTimeout { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// Default retry policy applied when a client does not specify one explicitly. + /// + public static RetryPolicy DefaultRetryPolicy { get; set; } = RetryPolicy.Default; +} diff --git a/src/Weaviate.Client/packages.lock.json b/src/Weaviate.Client/packages.lock.json index a86a869d..e63f8174 100644 --- a/src/Weaviate.Client/packages.lock.json +++ b/src/Weaviate.Client/packages.lock.json @@ -40,6 +40,19 @@ "resolved": "2.76.0", "contentHash": "goRzYZVMgQtyLvkWdgTnPycg49hlNxnMkqGGgR3l7nCOm0bUh0YeAneiJ9JFk3XLgF4suQUdETYkl2Mg/TBr0w==" }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Direct", + "requested": "[9.0.8, )", + "resolved": "9.0.8", + "contentHash": "WNrad20tySNCPe9aJUK7Wfwh+RiyLF+id02FKW8Qfc+HAzNQHazcqMXAbwG/kmbS89uvan/nKK1MufkRahjrJA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8" + } + }, "Microsoft.Extensions.Http": { "type": "Direct", "requested": "[9.0.8, )", @@ -68,6 +81,16 @@ "System.Text.Json": "9.0.8" } }, + "Microsoft.Extensions.Options": { + "type": "Direct", + "requested": "[9.0.8, )", + "resolved": "9.0.8", + "contentHash": "OmTaQ0v4gxGQkehpwWIqPoEiwsPuG/u4HUsbOFoWGx4DKET2AXzopnFe/fE608FIhzc/kcg2p8JdyMRCCUzitQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, "MinVer": { "type": "Direct", "requested": "[6.0.0, )", @@ -160,6 +183,14 @@ "System.Diagnostics.DiagnosticSource": "9.0.8" } }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "4zZbQ4w+hCMm9J+z5NOj3giIPT2MhZxx05HX/MGuAmDBbjOuXlYIIRN+t4V6OLxy5nXZIcXO+dQMB/OWubuDkw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "9.0.8", @@ -194,15 +225,6 @@ "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.8" } }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.8", - "contentHash": "OmTaQ0v4gxGQkehpwWIqPoEiwsPuG/u4HUsbOFoWGx4DKET2AXzopnFe/fE608FIhzc/kcg2p8JdyMRCCUzitQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", - "Microsoft.Extensions.Primitives": "9.0.8" - } - }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "9.0.8", From 257ccc9c4376f8e86508a68c7c06b4146264572f Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 26 Nov 2025 17:23:54 +0100 Subject: [PATCH 09/14] feat: Refactor WeaviateClient to remove token service factory and streamline async initialization --- src/Weaviate.Client/ClientConfiguration.cs | 43 ++--------- .../WeaviateClientFactory.cs | 2 +- src/Weaviate.Client/WeaviateClient.cs | 75 +++---------------- src/Weaviate.Client/WeaviateClientBuilder.cs | 5 +- 4 files changed, 23 insertions(+), 102 deletions(-) diff --git a/src/Weaviate.Client/ClientConfiguration.cs b/src/Weaviate.Client/ClientConfiguration.cs index 4528460c..e725497d 100644 --- a/src/Weaviate.Client/ClientConfiguration.cs +++ b/src/Weaviate.Client/ClientConfiguration.cs @@ -18,7 +18,7 @@ public sealed record ClientConfiguration( TimeSpan? QueryTimeout = null, RetryPolicy? RetryPolicy = null, DelegatingHandler[]? CustomHandlers = null, - ITokenServiceFactory? TokenServiceFactory = null + HttpMessageHandler? HttpMessageHandler = null ) { public Uri RestUri => @@ -43,47 +43,18 @@ public sealed record ClientConfiguration( /// Builds a WeaviateClient asynchronously, initializing all services in the correct order. /// This is the recommended way to create clients. /// - internal async Task BuildAsync(HttpMessageHandler? messageHandler = null) + internal async Task BuildAsync() { var logger = LoggerFactory .Create(builder => builder.AddConsole()) .CreateLogger(); - // Use factory to create token service - var tokenService = await ( - TokenServiceFactory ?? new DefaultTokenServiceFactory() - ).CreateAsync(this); + // Create client - it will initialize itself via PerformInitializationAsync + var client = new WeaviateClient(this, logger); - // Create REST client - var restClient = WeaviateClient.CreateRestClient( - this, - messageHandler, - tokenService, - logger - ); + // Wait for initialization to complete + await client.InitializeAsync(); - // Fetch metadata eagerly with init timeout - this will throw if authentication fails - var initTimeout = InitTimeout ?? DefaultTimeout ?? WeaviateDefaults.DefaultTimeout; - var metaCts = new CancellationTokenSource(initTimeout); - var metaDto = await restClient.GetMeta(metaCts.Token); - var meta = new Models.MetaInfo - { - GrpcMaxMessageSize = metaDto?.GrpcMaxMessageSize is not null - ? Convert.ToUInt64(metaDto.GrpcMaxMessageSize) - : null, - Hostname = metaDto?.Hostname ?? string.Empty, - Version = - Models.MetaInfo.ParseWeaviateVersion(metaDto?.Version ?? string.Empty) - ?? new System.Version(0, 0), - Modules = metaDto?.Modules?.ToDictionary() ?? [], - }; - - var maxMessageSize = meta.GrpcMaxMessageSize; - - // Create gRPC client with metadata - var grpcClient = WeaviateClient.CreateGrpcClient(this, tokenService, maxMessageSize); - - // Create and return the client with pre-built services - return new WeaviateClient(this, restClient, grpcClient, logger, meta); + return client; } }; diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs b/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs index 76508766..b7816377 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs @@ -70,7 +70,7 @@ private async Task CreateClientAsync(string name) var logger = _loggerFactory.CreateLogger(); var clientOptions = Options.Create(options); - var client = new WeaviateClient(clientOptions, null, logger); + var client = new WeaviateClient(clientOptions, logger); // Initialize the client await client.InitializeAsync(); diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index ff0b8e06..324a6d50 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -21,7 +21,6 @@ public partial class WeaviateClient : IDisposable // Async initialization support private readonly Lazy? _initializationTask; - private readonly ITokenServiceFactory? _tokenServiceFactory; private readonly ClientConfiguration? _configForAsyncInit; public async Task GetMeta(CancellationToken cancellationToken = default) @@ -146,66 +145,21 @@ static bool IsWeaviateDomain(string url) public TimeSpan? QueryTimeout => Configuration.QueryTimeout; /// - /// Creates a WeaviateClient with the given configuration and services. - /// Internal method used by the builder pattern. + /// Internal constructor for builder path with async initialization. /// - internal WeaviateClient( - ClientConfiguration configuration, - WeaviateRestClient restClient, - WeaviateGrpcClient grpcClient, - ILogger? logger = null, - Models.MetaInfo? meta = null - ) + internal WeaviateClient(ClientConfiguration configuration, ILogger? logger) { - _logger = logger ?? _logger; Configuration = configuration; - RestClient = restClient; - GrpcClient = grpcClient; - _metaCache = meta; - - Cluster = new ClusterClient(this); - Collections = new CollectionsClient(this); - Alias = new AliasClient(this); - Users = new UsersClient(this); - Roles = new RolesClient(this); - Groups = new GroupsClient(this); - } - - /// - /// Creates a WeaviateClient from configuration and optional HTTP message handler. - /// For backward compatibility with existing code that creates clients directly. - /// - public WeaviateClient( - ClientConfiguration? configuration = null, - HttpMessageHandler? httpMessageHandler = null, - ITokenServiceFactory? tokenServiceFactory = null, - ILogger? logger = null - ) - { - var config = configuration ?? DefaultOptions; - - var tokenService = tokenServiceFactory?.CreateSync(config); - - var loggerInstance = + _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - var restClient = CreateRestClientForPublic( - config, - httpMessageHandler, - tokenService, - loggerInstance - ); - var grpcClientInstance = CreateGrpcClientForPublic(config, tokenService); - - // Initialize like the internal constructor - _logger = loggerInstance; - Configuration = config; - RestClient = restClient; - GrpcClient = grpcClientInstance; + // Initialize Lazy task that will run initialization on first access + _initializationTask = new Lazy(() => PerformInitializationAsync(configuration)); - Cluster = new ClusterClient(this); + // Initialize client facades Collections = new CollectionsClient(this); + Cluster = new ClusterClient(this); Alias = new AliasClient(this); Users = new UsersClient(this); Roles = new RolesClient(this); @@ -255,7 +209,7 @@ internal WeaviateClient( /// Uses async initialization pattern - call InitializeAsync() or ensure IHostedService runs. /// public WeaviateClient(IOptions options) - : this(options, null, null) { } + : this(options, null) { } /// /// Constructor for dependency injection scenarios with logger. @@ -263,7 +217,6 @@ public WeaviateClient(IOptions options) /// public WeaviateClient( IOptions options, - ITokenServiceFactory? tokenServiceFactory, ILogger? logger ) { @@ -274,8 +227,6 @@ public WeaviateClient( logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - _tokenServiceFactory = tokenServiceFactory; - // Initialize Lazy task that will run initialization on first access _initializationTask = new Lazy(() => PerformInitializationAsync(_configForAsyncInit)); @@ -295,13 +246,11 @@ private async Task PerformInitializationAsync(ClientConfiguration config) { _logger.LogDebug("Starting Weaviate client initialization..."); - // Initialize token service asynchronously - var tokenService = await ( - _tokenServiceFactory ?? new DefaultTokenServiceFactory() - ).CreateAsync(config); + // Initialize token service asynchronously - always use DefaultTokenServiceFactory + var tokenService = await new DefaultTokenServiceFactory().CreateAsync(config); - // Create REST client - RestClient = CreateRestClient(config, null, tokenService, _logger); + // Create REST client - get HttpMessageHandler from config + RestClient = CreateRestClient(config, config.HttpMessageHandler, tokenService, _logger); // Fetch metadata eagerly with init timeout - this will throw if authentication fails var initTimeout = diff --git a/src/Weaviate.Client/WeaviateClientBuilder.cs b/src/Weaviate.Client/WeaviateClientBuilder.cs index ee6c0c28..208ae5f1 100644 --- a/src/Weaviate.Client/WeaviateClientBuilder.cs +++ b/src/Weaviate.Client/WeaviateClientBuilder.cs @@ -281,10 +281,11 @@ public async Task BuildAsync() _insertTimeout, _queryTimeout, _retryPolicy, - _customHandlers.Count > 0 ? _customHandlers.ToArray() : null + _customHandlers.Count > 0 ? _customHandlers.ToArray() : null, + _httpMessageHandler ); - return await config.BuildAsync(_httpMessageHandler); + return await config.BuildAsync(); } /// From 15eb2159a26f201cb7ba685f1d61b21703b4949e Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 26 Nov 2025 17:40:38 +0100 Subject: [PATCH 10/14] feat: Ensure client initialization in all client operations --- docs/CLIENT_INIT.md | 195 +++++++++++++++++++++++ src/Weaviate.Client/AliasClient.cs | 5 + src/Weaviate.Client/ClusterClient.cs | 1 + src/Weaviate.Client/CollectionClient.cs | 2 + src/Weaviate.Client/CollectionsClient.cs | 6 + src/Weaviate.Client/RolesClient.cs | 15 +- src/Weaviate.Client/UsersClient.cs | 1 + src/Weaviate.Client/WeaviateClient.cs | 2 +- 8 files changed, 224 insertions(+), 3 deletions(-) diff --git a/docs/CLIENT_INIT.md b/docs/CLIENT_INIT.md index 7538dbc9..9b36a8e1 100644 --- a/docs/CLIENT_INIT.md +++ b/docs/CLIENT_INIT.md @@ -100,6 +100,201 @@ Client initialization is asynchronous because: 3. **Thread Safety**: Async operations don't block thread pool threads, improving application responsiveness 4. **Error Handling**: Connection and authentication issues surface immediately during client creation, not later during operations +## Initialization Lifecycle + +### Builder Pattern (Recommended) + +When using `WeaviateClientBuilder` or `Connect` helpers, initialization happens automatically: + +```csharp +// Client is fully initialized before being returned +var client = await WeaviateClientBuilder.Local().BuildAsync(); +// ✅ RestClient and GrpcClient are ready to use +var results = await client.Collections.Get("Article").Query.FetchObjects(); +``` + +**Key Points:** +- `BuildAsync()` calls `InitializeAsync()` internally before returning +- The client is **always fully initialized** when you receive it +- No manual initialization needed + +### Dependency Injection Pattern + +When using dependency injection, there are two modes: + +#### Eager Initialization (Default - Recommended) + +```csharp +services.AddWeaviateLocal( + hostname: "localhost", + eagerInitialization: true // Default +); +``` + +**How it works:** +- A hosted service (`WeaviateInitializationService`) runs on application startup +- Calls `InitializeAsync()` automatically before your app serves requests +- The client is **always initialized** when injected into services + +```csharp +public class MyService +{ + private readonly WeaviateClient _client; + + public MyService(WeaviateClient client) + { + // ✅ Client is already initialized by the hosted service + _client = client; + } + + public async Task DoWork() + { + // ✅ Safe to use immediately + var results = await _client.Collections.Get("Article").Query.FetchObjects(); + } +} +``` + +#### Lazy Initialization (Manual Control) + +```csharp +services.AddWeaviateLocal( + hostname: "localhost", + eagerInitialization: false // Opt-in to lazy initialization +); +``` + +**How it works:** +- Client is created but NOT initialized +- You must call `InitializeAsync()` before first use +- Useful for scenarios where you want to control when initialization happens + +```csharp +public class MyService +{ + private readonly WeaviateClient _client; + + public MyService(WeaviateClient client) + { + // ⚠️ Client is NOT yet initialized + _client = client; + } + + public async Task Initialize() + { + // ✅ Manually trigger initialization + await _client.InitializeAsync(); + } + + public async Task DoWork() + { + // Check if initialized + if (!_client.IsInitialized) + { + await _client.InitializeAsync(); + } + + var results = await _client.Collections.Get("Article").Query.FetchObjects(); + } +} +``` + +### Checking Initialization Status + +Use the `IsInitialized` property to check if the client is ready: + +```csharp +if (client.IsInitialized) +{ + // Safe to use RestClient and GrpcClient + var results = await client.Collections.Get("Article").Query.FetchObjects(); +} +else +{ + // Must call InitializeAsync() first + await client.InitializeAsync(); +} +``` + +**What happens during initialization:** +1. Token service is created (for authentication) +2. REST client is configured +3. Server metadata is fetched (validates auth, gets gRPC settings) +4. gRPC client is configured with server metadata +5. `RestClient` and `GrpcClient` properties are populated + +### Important: When is the Client Ready? + +| Pattern | When Ready | RestClient/GrpcClient Available | +|---------|-----------|--------------------------------| +| `await BuildAsync()` | Immediately after return | ✅ Yes | +| DI Eager (default) | Before app starts serving | ✅ Yes | +| DI Lazy | After calling `InitializeAsync()` | ⚠️ Only after init | + +**⚠️ Using uninitialized client causes `NullReferenceException`:** + +```csharp +// ❌ BAD: Lazy DI without calling InitializeAsync() +var client = serviceProvider.GetService(); +var results = await client.Collections.Get("Article").Query.FetchObjects(); // NullReferenceException! + +// ✅ GOOD: Check and initialize if needed +var client = serviceProvider.GetService(); +if (!client.IsInitialized) +{ + await client.InitializeAsync(); +} +var results = await client.Collections.Get("Article").Query.FetchObjects(); +``` + +### Automatic Initialization Guards + +**✨ Safety Feature:** All public async methods in the Weaviate client automatically ensure initialization before executing. This provides a safety net against accidental use of uninitialized clients. + +**How it works:** + +When you call any async method on the client (like `Collections.Create()`, `Cluster.Replicate()`, `Alias.Get()`, etc.), the client automatically calls `EnsureInitializedAsync()` internally before performing the operation. If the client isn't initialized yet: + +- **With eager initialization (default)**: The guard passes immediately since the client is already initialized +- **With lazy initialization**: The guard triggers initialization automatically on first use + +```csharp +// Lazy DI - initialization happens automatically on first call +services.AddWeaviateLocal(hostname: "localhost", eagerInitialization: false); + +// Later in your code... +var client = serviceProvider.GetService(); + +// ✅ This works! The client auto-initializes on first use +var collections = await client.Collections.List(); // Initialization happens here automatically +``` + +**Performance Impact:** + +- **Eager initialization (default)**: No overhead - guards pass through immediately +- **Lazy initialization**: Minimal overhead - first call triggers initialization, subsequent calls pass through +- Guards use `Lazy` internally, ensuring initialization happens exactly once even with concurrent calls + +**When guards help:** + +- Forgetting to call `InitializeAsync()` in lazy initialization scenarios +- Race conditions where multiple threads access an uninitialized client +- Unit tests that don't properly set up client initialization + +**What's protected:** + +- ✅ `client.Collections.*` - All collection operations +- ✅ `client.Cluster.*` - All cluster operations +- ✅ `client.Alias.*` - All alias operations +- ✅ `client.Users.*`, `client.Roles.*`, `client.Groups.*` - All auth operations +- ✅ Collection-level operations like `collection.Delete()`, `collection.Iterator()` +- ✅ All async methods that access REST or gRPC clients + +**What's not protected:** + +- ❌ Synchronous property accessors (design limitation - properties can't be async) +- ❌ Direct access to internal clients (not part of public API) + ## Connection Configuration ### Local Development diff --git a/src/Weaviate.Client/AliasClient.cs b/src/Weaviate.Client/AliasClient.cs index 07100e91..d104048d 100644 --- a/src/Weaviate.Client/AliasClient.cs +++ b/src/Weaviate.Client/AliasClient.cs @@ -22,6 +22,7 @@ internal AliasClient(WeaviateClient client) /// The alias with its target collection public async Task Get(string aliasName, CancellationToken cancellationToken = default) { + await _client.EnsureInitializedAsync(); var dto = await _client.RestClient.AliasGet(aliasName, cancellationToken); return dto != null ? ToModel(dto) : null; } @@ -34,6 +35,7 @@ internal AliasClient(WeaviateClient client) /// The created alias public async Task Add(Alias alias, CancellationToken cancellationToken = default) { + await _client.EnsureInitializedAsync(); var dto = ToDto(alias); var result = await _client.RestClient.CollectionAliasesPost(dto, cancellationToken); return ToModel(result); @@ -50,6 +52,7 @@ public async Task> List( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dtos = await _client.RestClient.CollectionAliasesGet(collectionName, cancellationToken); return dtos.Select(ToModel); } @@ -67,6 +70,7 @@ public async Task Update( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dto = await _client.RestClient.AliasPut(aliasName, targetCollection, cancellationToken); return ToModel(dto); } @@ -78,6 +82,7 @@ public async Task Update( /// Cancellation token for the operation public async Task Delete(string aliasName, CancellationToken cancellationToken = default) { + await _client.EnsureInitializedAsync(); return await _client.RestClient.AliasDelete(aliasName, cancellationToken); } diff --git a/src/Weaviate.Client/ClusterClient.cs b/src/Weaviate.Client/ClusterClient.cs index 2b0cc472..7511b47a 100644 --- a/src/Weaviate.Client/ClusterClient.cs +++ b/src/Weaviate.Client/ClusterClient.cs @@ -36,6 +36,7 @@ public async Task Replicate( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dto = new Rest.Dto.ReplicationReplicateReplicaRequest { Collection = request.Collection, diff --git a/src/Weaviate.Client/CollectionClient.cs b/src/Weaviate.Client/CollectionClient.cs index d08f8560..c8bed89c 100644 --- a/src/Weaviate.Client/CollectionClient.cs +++ b/src/Weaviate.Client/CollectionClient.cs @@ -61,6 +61,7 @@ internal CollectionClient( /// Cancellation token to cancel the operation. public async Task Delete(CancellationToken cancellationToken = default) { + await _client.EnsureInitializedAsync(); await _client.RestClient.CollectionDelete(Name, cancellationToken); } @@ -74,6 +75,7 @@ public async IAsyncEnumerable Iterator( [EnumeratorCancellation] CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); Guid? cursor = after; while (true) diff --git a/src/Weaviate.Client/CollectionsClient.cs b/src/Weaviate.Client/CollectionsClient.cs index 3f6eb366..302d8fbf 100644 --- a/src/Weaviate.Client/CollectionsClient.cs +++ b/src/Weaviate.Client/CollectionsClient.cs @@ -16,6 +16,7 @@ public async Task Create( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var response = await _client.RestClient.CollectionCreate( collection.ToDto(), cancellationToken @@ -31,6 +32,7 @@ public async Task Create( ) where T : class, new() { + await _client.EnsureInitializedAsync(); var response = await _client.RestClient.CollectionCreate( collection.ToDto(), cancellationToken @@ -45,6 +47,7 @@ public async Task Delete(string collectionName, CancellationToken cancellationTo { ArgumentException.ThrowIfNullOrEmpty(collectionName); + await _client.EnsureInitializedAsync(); await _client.RestClient.CollectionDelete(collectionName, cancellationToken); } @@ -62,6 +65,7 @@ public async Task Exists( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); return await _client.RestClient.CollectionExists(collectionName, cancellationToken); } @@ -70,6 +74,7 @@ public async Task Exists( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var response = await _client.RestClient.CollectionGet(collectionName, cancellationToken); if (response is null) @@ -85,6 +90,7 @@ public async Task Exists( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var response = await _client.RestClient.CollectionList(cancellationToken); foreach (var c in response?.Classes ?? Enumerable.Empty()) diff --git a/src/Weaviate.Client/RolesClient.cs b/src/Weaviate.Client/RolesClient.cs index 7792ccb0..ef7a3067 100644 --- a/src/Weaviate.Client/RolesClient.cs +++ b/src/Weaviate.Client/RolesClient.cs @@ -13,12 +13,14 @@ public class RolesClient public async Task> ListAll(CancellationToken cancellationToken = default) { + await _client.EnsureInitializedAsync(); var roles = await _client.RestClient.RolesList(cancellationToken); return roles.Select(r => r.ToModel()); } public async Task Get(string id, CancellationToken cancellationToken = default) { + await _client.EnsureInitializedAsync(); var role = await _client.RestClient.RoleGet(id, cancellationToken); return role is null ? null : role.ToModel(); } @@ -29,6 +31,7 @@ public async Task Create( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dto = new Rest.Dto.Role { Name = name, @@ -38,8 +41,11 @@ public async Task Create( return created.ToModel(); } - public Task Delete(string id, CancellationToken cancellationToken = default) => - _client.RestClient.RoleDelete(id, cancellationToken); + public async Task Delete(string id, CancellationToken cancellationToken = default) + { + await _client.EnsureInitializedAsync(); + await _client.RestClient.RoleDelete(id, cancellationToken); + } public async Task AddPermissions( string id, @@ -47,6 +53,7 @@ public async Task AddPermissions( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dtos = permissions.SelectMany(p => p.ToDto()).ToList(); var updated = await _client.RestClient.RoleAddPermissions(id, dtos, cancellationToken); return updated.ToModel(); @@ -58,6 +65,7 @@ public async Task RemovePermissions( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dtos = permissions.SelectMany(p => p.ToDto()).ToList(); var updated = await _client.RestClient.RoleRemovePermissions(id, dtos, cancellationToken); return updated.ToModel(); @@ -69,6 +77,7 @@ public async Task HasPermission( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dto = permission.ToDto().Single(); return await _client.RestClient.RoleHasPermission(id, dto, cancellationToken); } @@ -78,6 +87,7 @@ public async Task> GetUserAssignments( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var list = await _client.RestClient.RoleUserAssignments(roleId, cancellationToken); return list.Select(a => a.ToModel()); } @@ -87,6 +97,7 @@ public async Task> GetGroupAssignments( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var list = await _client.RestClient.RoleGroupAssignments(roleId, cancellationToken); return list.Select(a => a.ToModel()); } diff --git a/src/Weaviate.Client/UsersClient.cs b/src/Weaviate.Client/UsersClient.cs index 9e1f054d..e4d7834c 100644 --- a/src/Weaviate.Client/UsersClient.cs +++ b/src/Weaviate.Client/UsersClient.cs @@ -32,6 +32,7 @@ internal UsersClient(WeaviateClient client) /// public async Task OwnInfo(CancellationToken cancellationToken = default) { + await _client.EnsureInitializedAsync(); var dto = await _client.RestClient.UserOwnInfoGet(cancellationToken); if (dto is null) return null; diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index 324a6d50..0d9cf193 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -308,7 +308,7 @@ public Task InitializeAsync(CancellationToken cancellationToken = default) /// Helper to ensure initialization before using the client. /// Throws if initialization failed. /// - private async Task EnsureInitializedAsync() + internal async Task EnsureInitializedAsync() { if (_initializationTask != null) { From f29faa13ea17b5fd521c508ce8c2ebf0fc94086b Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 26 Nov 2025 18:05:53 +0100 Subject: [PATCH 11/14] Move doc files --- DEPENDENCY_INJECTION.md => docs/DEPENDENCY_INJECTION.md | 0 MULTIPLE_CLIENTS.md => docs/MULTIPLE_CLIENTS.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename DEPENDENCY_INJECTION.md => docs/DEPENDENCY_INJECTION.md (100%) rename MULTIPLE_CLIENTS.md => docs/MULTIPLE_CLIENTS.md (100%) diff --git a/DEPENDENCY_INJECTION.md b/docs/DEPENDENCY_INJECTION.md similarity index 100% rename from DEPENDENCY_INJECTION.md rename to docs/DEPENDENCY_INJECTION.md diff --git a/MULTIPLE_CLIENTS.md b/docs/MULTIPLE_CLIENTS.md similarity index 100% rename from MULTIPLE_CLIENTS.md rename to docs/MULTIPLE_CLIENTS.md From 93f2812d8969d67b3f9d6ee62fd18721444eed9e Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 26 Nov 2025 23:25:32 +0100 Subject: [PATCH 12/14] fix: file formatting --- .../DependencyInjection/WeaviateInitializationService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs b/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs index 6fb143b9..868c8f75 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs @@ -14,7 +14,8 @@ internal class WeaviateInitializationService : IHostedService public WeaviateInitializationService( WeaviateClient client, - ILogger logger) + ILogger logger + ) { _client = client; _logger = logger; @@ -32,7 +33,8 @@ public async Task StartAsync(CancellationToken cancellationToken) var version = _client.WeaviateVersion; _logger.LogInformation( "Weaviate client initialized successfully. Server version: {Version}", - version); + version + ); } catch (Exception ex) { From 52d527af4afdb480fec63ddb41a5ba41747c0dae Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Thu, 27 Nov 2025 14:55:53 +0100 Subject: [PATCH 13/14] feat: Add warning log for unsupported Weaviate server versions --- src/Weaviate.Client/WeaviateClient.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index 0d9cf193..264f56ae 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -269,6 +269,17 @@ private async Task PerformInitializationAsync(ClientConfiguration config) Modules = metaDto?.Modules?.ToDictionary() ?? [], }; + // Log warning if connecting to a server older than 1.31.0 + var minSupportedVersion = new Version(1, 31, 0); + if (_metaCache.HasValue && _metaCache.Value.Version < minSupportedVersion) + { + _logger.LogWarning( + "Connected to Weaviate server version {ServerVersion}, which is earlier than the minimum supported version {MinVersion}. Some features may not work as expected.", + _metaCache.Value.Version, + minSupportedVersion + ); + } + var maxMessageSize = _metaCache?.GrpcMaxMessageSize; // Create gRPC client with metadata From 08d236064f832714d8444c5fcc035588ca304921 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Tue, 2 Dec 2025 11:09:35 +0100 Subject: [PATCH 14/14] feat: Rename DataTimeout to InsertTimeout and update related documentation --- docs/DEPENDENCY_INJECTION.md | 20 +++++- src/Weaviate.Client/ClientConfiguration.cs | 2 +- .../DependencyInjection/WeaviateOptions.cs | 4 +- .../WeaviateServiceCollectionExtensions.cs | 24 +++---- src/Weaviate.Client/WeaviateClient.cs | 2 +- src/Weaviate.Client/WeaviateClientBuilder.cs | 63 ------------------- src/Weaviate.Client/WeaviateDefaults.cs | 4 +- 7 files changed, 35 insertions(+), 84 deletions(-) diff --git a/docs/DEPENDENCY_INJECTION.md b/docs/DEPENDENCY_INJECTION.md index 2f27e3df..14ebffcd 100644 --- a/docs/DEPENDENCY_INJECTION.md +++ b/docs/DEPENDENCY_INJECTION.md @@ -52,7 +52,7 @@ var app = builder.Build(); "UseSsl": false, "DefaultTimeout": "00:00:30", "InitTimeout": "00:00:02", - "DataTimeout": "00:02:00", + "InsertTimeout": "00:02:00", "QueryTimeout": "00:01:00" } } @@ -108,7 +108,7 @@ public class CatService | `Credentials` | `ICredentials?` | `null` | Authentication credentials | | `DefaultTimeout` | `TimeSpan?` | `30s` | Default timeout for all operations | | `InitTimeout` | `TimeSpan?` | `2s` | Timeout for initialization | -| `DataTimeout` | `TimeSpan?` | `120s` | Timeout for data operations | +| `InsertTimeout` | `TimeSpan?` | `120s` | Timeout for data operations | | `QueryTimeout` | `TimeSpan?` | `60s` | Timeout for query operations | | `Headers` | `Dictionary?` | `null` | Additional HTTP headers | | `RetryPolicy` | `RetryPolicy?` | Default | Retry policy for failed requests | @@ -150,11 +150,13 @@ builder.Services.AddWeaviate(options => { ... }, eagerInitialization: true); ``` **Benefits:** + - ✅ Client is ready when first request arrives - ✅ Fails fast if connection issues exist - ✅ Simpler usage - no need to await initialization **How it works:** + 1. `WeaviateClient` is constructed with DI 2. `IHostedService` runs on app startup 3. Calls `client.InitializeAsync()` which: @@ -172,11 +174,13 @@ builder.Services.AddWeaviate(options => { ... }, eagerInitialization: false); ``` **When to use:** + - Application startup time is critical - Weaviate connection isn't needed immediately - You want to handle connection failures gracefully **Usage with lazy initialization:** + ```csharp public class MyService { @@ -222,6 +226,7 @@ var client = await Connect.Local( ``` These methods: + - ✅ Return a fully initialized client - ✅ Use async initialization (no blocking) - ✅ Follow the same REST → Meta → gRPC initialization flow @@ -345,6 +350,7 @@ public class MyIntegrationTests : IAsyncLifetime - Runs initialization task (only once, thread-safe) 3. **Initialization Task** + ``` ┌─────────────────────────────────────────┐ │ 1. Initialize Token Service (OAuth, etc)│ @@ -366,9 +372,10 @@ public class MyIntegrationTests : IAsyncLifetime - `EnsureInitializedAsync()` returns immediately (already initialized) - No performance penalty -### No More `GetAwaiter().GetResult()`! +### No More `GetAwaiter().GetResult()` The old pattern had to block: + ```csharp // ❌ Old: Blocking async call in constructor var client = new WeaviateClient(config); @@ -376,6 +383,7 @@ var meta = GetMetaAsync().GetAwaiter().GetResult(); // Deadlock risk! ``` The new pattern is fully async: + ```csharp // ✅ New: Lazy async initialization var client = new WeaviateClient(options); @@ -390,6 +398,7 @@ await client.InitializeAsync(); // Or called automatically ### From Old Constructor Pattern **Before:** + ```csharp var config = new ClientConfiguration { @@ -401,6 +410,7 @@ var client = new WeaviateClient(config); ``` **After (DI):** + ```csharp builder.Services.AddWeaviate(options => { @@ -413,6 +423,7 @@ public MyService(WeaviateClient client) { ... } ``` **After (Non-DI):** + ```csharp var client = await Connect.Local(); // Or @@ -424,6 +435,7 @@ await client.InitializeAsync(); ### From `WeaviateClientBuilder` **Before:** + ```csharp var client = await WeaviateClientBuilder .Local() @@ -432,6 +444,7 @@ var client = await WeaviateClientBuilder ``` **After (still works!):** + ```csharp // This pattern still works exactly as before var client = await WeaviateClientBuilder @@ -441,6 +454,7 @@ var client = await WeaviateClientBuilder ``` **Or with DI:** + ```csharp builder.Services.AddWeaviate(options => { diff --git a/src/Weaviate.Client/ClientConfiguration.cs b/src/Weaviate.Client/ClientConfiguration.cs index e725497d..5ef143f2 100644 --- a/src/Weaviate.Client/ClientConfiguration.cs +++ b/src/Weaviate.Client/ClientConfiguration.cs @@ -14,7 +14,7 @@ public sealed record ClientConfiguration( ICredentials? Credentials = null, TimeSpan? DefaultTimeout = null, TimeSpan? InitTimeout = null, - TimeSpan? DataTimeout = null, + TimeSpan? InsertTimeout = null, TimeSpan? QueryTimeout = null, RetryPolicy? RetryPolicy = null, DelegatingHandler[]? CustomHandlers = null, diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs index dfc64b4a..4fd1b7e1 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateOptions.cs @@ -63,7 +63,7 @@ public class WeaviateOptions /// /// Timeout for data operations (Insert, Delete, Update, Reference management). /// - public TimeSpan? DataTimeout { get; set; } + public TimeSpan? InsertTimeout { get; set; } /// /// Timeout for query/search operations (FetchObjects, NearText, BM25, Hybrid, etc.). @@ -92,7 +92,7 @@ internal ClientConfiguration ToClientConfiguration() Credentials: Credentials, DefaultTimeout: DefaultTimeout, InitTimeout: InitTimeout, - DataTimeout: DataTimeout, + InsertTimeout: InsertTimeout, QueryTimeout: QueryTimeout, RetryPolicy: RetryPolicy ); diff --git a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs index 1f74dad9..cd7ccf0c 100644 --- a/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs +++ b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs @@ -108,7 +108,7 @@ public static IServiceCollection AddWeaviate( /// Additional HTTP headers to include in requests. /// Default timeout for all operations. /// Timeout for initialization operations. - /// Timeout for data operations. + /// Timeout for data operations. /// Timeout for query operations. /// Whether to initialize the client eagerly on application startup. Default is true. /// The service collection for method chaining. @@ -122,7 +122,7 @@ public static IServiceCollection AddWeaviateLocal( Dictionary? headers = null, TimeSpan? defaultTimeout = null, TimeSpan? initTimeout = null, - TimeSpan? dataTimeout = null, + TimeSpan? insertTimeout = null, TimeSpan? queryTimeout = null, bool eagerInitialization = true ) @@ -139,7 +139,7 @@ public static IServiceCollection AddWeaviateLocal( options.Headers = headers; options.DefaultTimeout = defaultTimeout; options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; + options.InsertTimeout = insertTimeout; options.QueryTimeout = queryTimeout; }, eagerInitialization @@ -158,7 +158,7 @@ public static IServiceCollection AddWeaviateLocal( /// Additional HTTP headers to include in requests. /// Default timeout for all operations. /// Timeout for initialization operations. - /// Timeout for data operations. + /// Timeout for data operations. /// Timeout for query operations. /// Whether to initialize the client eagerly on application startup. Default is true. /// The service collection for method chaining. @@ -169,7 +169,7 @@ public static IServiceCollection AddWeaviateCloud( Dictionary? headers = null, TimeSpan? defaultTimeout = null, TimeSpan? initTimeout = null, - TimeSpan? dataTimeout = null, + TimeSpan? insertTimeout = null, TimeSpan? queryTimeout = null, bool eagerInitialization = true ) @@ -186,7 +186,7 @@ public static IServiceCollection AddWeaviateCloud( options.Headers = headers; options.DefaultTimeout = defaultTimeout; options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; + options.InsertTimeout = insertTimeout; options.QueryTimeout = queryTimeout; }, eagerInitialization @@ -248,7 +248,7 @@ Action configureOptions /// Additional HTTP headers to include in requests. /// Default timeout for all operations. /// Timeout for initialization operations. - /// Timeout for data operations. + /// Timeout for data operations. /// Timeout for query operations. /// The service collection for method chaining. public static IServiceCollection AddWeaviateLocal( @@ -262,7 +262,7 @@ public static IServiceCollection AddWeaviateLocal( Dictionary? headers = null, TimeSpan? defaultTimeout = null, TimeSpan? initTimeout = null, - TimeSpan? dataTimeout = null, + TimeSpan? insertTimeout = null, TimeSpan? queryTimeout = null ) { @@ -279,7 +279,7 @@ public static IServiceCollection AddWeaviateLocal( options.Headers = headers; options.DefaultTimeout = defaultTimeout; options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; + options.InsertTimeout = insertTimeout; options.QueryTimeout = queryTimeout; } ); @@ -311,7 +311,7 @@ Action configureOptions /// Additional HTTP headers to include in requests. /// Default timeout for all operations. /// Timeout for initialization operations. - /// Timeout for data operations. + /// Timeout for data operations. /// Timeout for query operations. /// The service collection for method chaining. public static IServiceCollection AddWeaviateCloud( @@ -322,7 +322,7 @@ public static IServiceCollection AddWeaviateCloud( Dictionary? headers = null, TimeSpan? defaultTimeout = null, TimeSpan? initTimeout = null, - TimeSpan? dataTimeout = null, + TimeSpan? insertTimeout = null, TimeSpan? queryTimeout = null ) { @@ -339,7 +339,7 @@ public static IServiceCollection AddWeaviateCloud( options.Headers = headers; options.DefaultTimeout = defaultTimeout; options.InitTimeout = initTimeout; - options.DataTimeout = dataTimeout; + options.InsertTimeout = insertTimeout; options.QueryTimeout = queryTimeout; } ); diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index 264f56ae..7917bbfd 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -141,7 +141,7 @@ static bool IsWeaviateDomain(string url) public TimeSpan? DefaultTimeout => Configuration.DefaultTimeout; public TimeSpan? InitTimeout => Configuration.InitTimeout; - public TimeSpan? DataTimeout => Configuration.DataTimeout; + public TimeSpan? InsertTimeout => Configuration.InsertTimeout; public TimeSpan? QueryTimeout => Configuration.QueryTimeout; /// diff --git a/src/Weaviate.Client/WeaviateClientBuilder.cs b/src/Weaviate.Client/WeaviateClientBuilder.cs index 208ae5f1..4feda162 100644 --- a/src/Weaviate.Client/WeaviateClientBuilder.cs +++ b/src/Weaviate.Client/WeaviateClientBuilder.cs @@ -288,69 +288,6 @@ public async Task BuildAsync() return await config.BuildAsync(); } - /// - /// Synchronous build method - deprecated. Use BuildAsync() instead. - /// - [Obsolete( - "Use BuildAsync() instead. Synchronous initialization can cause blocking issues.", - false - )] - public WeaviateClient Build() - { - var loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole()); - var logger = loggerFactory.CreateLogger(); - - var config = new ClientConfiguration( - _restEndpoint, - _restPath, - _grpcEndpoint, - _grpcPath, - _restPort, - _grpcPort, - _useSsl, - _headers.Count > 0 ? new Dictionary(_headers) : null, - _credentials, - _defaultTimeout, - _initTimeout, - _insertTimeout, - _queryTimeout, - _retryPolicy, - _customHandlers.Count > 0 ? _customHandlers.ToArray() : null - ); - - // Initialize token service synchronously - var tokenService = ClientConfiguration.InitializeTokenServiceSync(config); - - // Create REST client - var restClient = WeaviateClient.CreateRestClient( - config, - _httpMessageHandler, - tokenService, - logger - ); - - // Fetch metadata synchronously (blocking) - ulong? maxMessageSize = null; - try - { - var metaDto = restClient.GetMeta(CancellationToken.None).GetAwaiter().GetResult(); - if (metaDto?.GrpcMaxMessageSize is not null) - { - maxMessageSize = Convert.ToUInt64(metaDto.GrpcMaxMessageSize); - } - } - catch - { - // If metadata fetch fails, use defaults - } - - // Create gRPC client with metadata - var grpcClient = WeaviateClient.CreateGrpcClient(config, tokenService, maxMessageSize); - - // Return the client with pre-built services - return new WeaviateClient(config, restClient, grpcClient, logger); - } - public static implicit operator Task(WeaviateClientBuilder builder) => builder.BuildAsync(); } diff --git a/src/Weaviate.Client/WeaviateDefaults.cs b/src/Weaviate.Client/WeaviateDefaults.cs index a491e9f7..29f8e3c3 100644 --- a/src/Weaviate.Client/WeaviateDefaults.cs +++ b/src/Weaviate.Client/WeaviateDefaults.cs @@ -19,9 +19,9 @@ public static class WeaviateDefaults /// /// Default timeout for data operations (Insert, Delete, Update, Reference management). Default is 120 seconds. - /// This can be overridden per client via ClientConfiguration.WithDataTimeout(). + /// This can be overridden per client via ClientConfiguration.WithInsertTimeout(). /// - public static TimeSpan DataTimeout { get; set; } = TimeSpan.FromSeconds(120); + public static TimeSpan InsertTimeout { get; set; } = TimeSpan.FromSeconds(120); /// /// Default timeout for query/search operations (FetchObjects, NearText, BM25, Hybrid, etc.). Default is 60 seconds.