diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e69de29b 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/docs/DEPENDENCY_INJECTION.md b/docs/DEPENDENCY_INJECTION.md new file mode 100644 index 00000000..14ebffcd --- /dev/null +++ b/docs/DEPENDENCY_INJECTION.md @@ -0,0 +1,478 @@ +# 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", + "InsertTimeout": "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 | +| `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 | + +### 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/docs/MULTIPLE_CLIENTS.md b/docs/MULTIPLE_CLIENTS.md new file mode 100644 index 00000000..4b79ed67 --- /dev/null +++ b/docs/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/DependencyInjectionExample.cs b/src/Example/DependencyInjectionExample.cs new file mode 100644 index 00000000..d2d89c02 --- /dev/null +++ b/src/Example/DependencyInjectionExample.cs @@ -0,0 +1,227 @@ +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 Run() + { + // Build host with dependency injection + 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 + 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 = 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 + { + // 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) + { + _logger.LogInformation( + " - {Name} ({Breed}, {Color})", + obj.Object?.Name, + obj.Object?.Breed, + obj.Object?.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/Example/DifferentConfigsExample.cs b/src/Example/DifferentConfigsExample.cs new file mode 100644 index 00000000..07c52841 --- /dev/null +++ b/src/Example/DifferentConfigsExample.cs @@ -0,0 +1,232 @@ +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 Run() + { + var host = Host.CreateDefaultBuilder() + .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 + 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 + } + ); + + 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/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 @@ + diff --git a/src/Example/MultipleClientsExample.cs b/src/Example/MultipleClientsExample.cs new file mode 100644 index 00000000..93d416ef --- /dev/null +++ b/src/Example/MultipleClientsExample.cs @@ -0,0 +1,222 @@ +using Microsoft.Extensions.Configuration; +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 Run() + { + var host = Host.CreateDefaultBuilder() + .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.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(); + + 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 = 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.Object!); + 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", + options => + context.Configuration.GetSection("Weaviate:Production").Bind(options) + ); + + services.AddWeaviateClient( + "staging", + options => + context.Configuration.GetSection("Weaviate:Staging").Bind(options) + ); + } + ) + .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/Example/Program.cs b/src/Example/Program.cs index ff87e679..251f5c88 100644 --- a/src/Example/Program.cs +++ b/src/Example/Program.cs @@ -1,200 +1,100 @@ -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.Run(); + 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.Run(); + 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..b22bc23c --- /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 = 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(); + } +} 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/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/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..5ef143f2 --- /dev/null +++ b/src/Weaviate.Client/ClientConfiguration.cs @@ -0,0 +1,60 @@ +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? InsertTimeout = null, + TimeSpan? QueryTimeout = null, + RetryPolicy? RetryPolicy = null, + DelegatingHandler[]? CustomHandlers = null, + HttpMessageHandler? HttpMessageHandler = 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() + { + var logger = LoggerFactory + .Create(builder => builder.AddConsole()) + .CreateLogger(); + + // Create client - it will initialize itself via PerformInitializationAsync + var client = new WeaviateClient(this, logger); + + // Wait for initialization to complete + await client.InitializeAsync(); + + return client; + } +}; diff --git a/src/Weaviate.Client/ClusterClient.cs b/src/Weaviate.Client/ClusterClient.cs index ce78ee94..7511b47a 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. @@ -35,6 +36,7 @@ public async Task Replicate( CancellationToken cancellationToken = default ) { + await _client.EnsureInitializedAsync(); var dto = new Rest.Dto.ReplicationReplicateReplicaRequest { Collection = request.Collection, @@ -47,11 +49,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 +70,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 +82,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/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 7fdc7028..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()) @@ -104,14 +110,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/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..b7816377 --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/WeaviateClientFactory.cs @@ -0,0 +1,97 @@ +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/WeaviateInitializationService.cs b/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs new file mode 100644 index 00000000..868c8f75 --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/WeaviateInitializationService.cs @@ -0,0 +1,51 @@ +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..4fd1b7e1 --- /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? InsertTimeout { 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, + InsertTimeout: InsertTimeout, + 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..cd7ccf0c --- /dev/null +++ b/src/Weaviate.Client/DependencyInjection/WeaviateServiceCollectionExtensions.cs @@ -0,0 +1,347 @@ +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 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. + /// 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 for a local Weaviate instance. + /// Similar to Connect.Local() but for dependency injection. + /// + /// 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. + /// 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( + this IServiceCollection services, + 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? insertTimeout = null, + TimeSpan? queryTimeout = 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; + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.InsertTimeout = insertTimeout; + options.QueryTimeout = queryTimeout; + }, + eagerInitialization + ); + + return services; + } + + /// + /// 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? insertTimeout = null, + TimeSpan? queryTimeout = 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); + options.Headers = headers; + options.DefaultTimeout = defaultTimeout; + options.InitTimeout = initTimeout; + options.InsertTimeout = insertTimeout; + options.QueryTimeout = queryTimeout; + }, + eagerInitialization + ); + + return services; + } + + // 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? insertTimeout = 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.InsertTimeout = insertTimeout; + 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? insertTimeout = 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.InsertTimeout = insertTimeout; + 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/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/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/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/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/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..7917bbfd 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; @@ -7,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(() => @@ -348,8 +19,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 +57,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)); } /// @@ -449,7 +127,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; } @@ -468,25 +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; + _logger = + logger + ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + // Initialize Lazy task that will run initialization on first access + _initializationTask = new Lazy(() => PerformInitializationAsync(configuration)); - Cluster = new ClusterClient(RestClient); + // Initialize client facades Collections = new CollectionsClient(this); + Cluster = new ClusterClient(this); Alias = new AliasClient(this); Users = new UsersClient(this); Roles = new RolesClient(this); @@ -494,13 +167,14 @@ internal WeaviateClient( } /// - /// Creates a WeaviateClient from configuration and optional HTTP message handler. - /// For backward compatibility with existing code that creates clients directly. + /// Internal constructor for testing with injected gRPC client. /// - public WeaviateClient( + internal WeaviateClient( ClientConfiguration? configuration = null, HttpMessageHandler? httpMessageHandler = null, - ILogger? logger = null + ITokenService? tokenService = null, + ILogger? logger = null, + WeaviateGrpcClient? grpcClient = null ) { var config = configuration ?? DefaultOptions; @@ -508,8 +182,13 @@ public WeaviateClient( 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 = grpcClient ?? CreateGrpcClientForPublic(config, tokenService); // Initialize like the internal constructor _logger = loggerInstance; @@ -517,7 +196,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); @@ -526,43 +205,135 @@ public WeaviateClient( } /// - /// Internal constructor for testing with injected gRPC client. + /// Constructor for dependency injection scenarios. + /// Uses async initialization pattern - call InitializeAsync() or ensure IHostedService runs. /// - internal WeaviateClient( - ClientConfiguration? configuration = null, - HttpMessageHandler? httpMessageHandler = null, - ILogger? logger = null, - WeaviateGrpcClient? grpcClient = null + 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 config = configuration ?? DefaultOptions; - var loggerInstance = + var weaviateOptions = options.Value; + _configForAsyncInit = weaviateOptions.ToClientConfiguration(); + Configuration = _configForAsyncInit; + _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - var restClient = CreateRestClientForPublic(config, httpMessageHandler, loggerInstance); - var grpcClientInstance = grpcClient ?? CreateGrpcClientForPublic(config); - - // 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(_configForAsyncInit)); - Cluster = new ClusterClient(RestClient); Collections = new CollectionsClient(this); + Cluster = new ClusterClient(this); 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 - always use DefaultTokenServiceFactory + var tokenService = await new DefaultTokenServiceFactory().CreateAsync(config); + + // 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 = + 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() ?? [], + }; + + // 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 + GrpcClient = CreateGrpcClient(config, tokenService, maxMessageSize); + + _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. + /// + internal async Task EnsureInitializedAsync() + { + if (_initializationTask != null) + { + await _initializationTask.Value; // Will throw if initialization failed + } + } + /// /// Helper to create REST client for the public constructor. /// private static WeaviateRestClient CreateRestClientForPublic( ClientConfiguration config, HttpMessageHandler? httpMessageHandler, + ITokenService? tokenService, ILogger? logger ) { @@ -570,16 +341,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/WeaviateClientBuilder.cs b/src/Weaviate.Client/WeaviateClientBuilder.cs index ee6c0c28..4feda162 100644 --- a/src/Weaviate.Client/WeaviateClientBuilder.cs +++ b/src/Weaviate.Client/WeaviateClientBuilder.cs @@ -281,73 +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); - } - - /// - /// 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); + return await config.BuildAsync(); } public static implicit operator Task(WeaviateClientBuilder builder) => diff --git a/src/Weaviate.Client/WeaviateDefaults.cs b/src/Weaviate.Client/WeaviateDefaults.cs new file mode 100644 index 00000000..29f8e3c3 --- /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.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; +} 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",