Skip to content

Commit 58d3d05

Browse files
committed
feat: Ensure client initialization in all client operations
1 parent aa526ff commit 58d3d05

File tree

8 files changed

+224
-3
lines changed

8 files changed

+224
-3
lines changed

docs/CLIENT_INIT.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,201 @@ Client initialization is asynchronous because:
100100
3. **Thread Safety**: Async operations don't block thread pool threads, improving application responsiveness
101101
4. **Error Handling**: Connection and authentication issues surface immediately during client creation, not later during operations
102102

103+
## Initialization Lifecycle
104+
105+
### Builder Pattern (Recommended)
106+
107+
When using `WeaviateClientBuilder` or `Connect` helpers, initialization happens automatically:
108+
109+
```csharp
110+
// Client is fully initialized before being returned
111+
var client = await WeaviateClientBuilder.Local().BuildAsync();
112+
// ✅ RestClient and GrpcClient are ready to use
113+
var results = await client.Collections.Get("Article").Query.FetchObjects();
114+
```
115+
116+
**Key Points:**
117+
- `BuildAsync()` calls `InitializeAsync()` internally before returning
118+
- The client is **always fully initialized** when you receive it
119+
- No manual initialization needed
120+
121+
### Dependency Injection Pattern
122+
123+
When using dependency injection, there are two modes:
124+
125+
#### Eager Initialization (Default - Recommended)
126+
127+
```csharp
128+
services.AddWeaviateLocal(
129+
hostname: "localhost",
130+
eagerInitialization: true // Default
131+
);
132+
```
133+
134+
**How it works:**
135+
- A hosted service (`WeaviateInitializationService`) runs on application startup
136+
- Calls `InitializeAsync()` automatically before your app serves requests
137+
- The client is **always initialized** when injected into services
138+
139+
```csharp
140+
public class MyService
141+
{
142+
private readonly WeaviateClient _client;
143+
144+
public MyService(WeaviateClient client)
145+
{
146+
// ✅ Client is already initialized by the hosted service
147+
_client = client;
148+
}
149+
150+
public async Task DoWork()
151+
{
152+
// ✅ Safe to use immediately
153+
var results = await _client.Collections.Get("Article").Query.FetchObjects();
154+
}
155+
}
156+
```
157+
158+
#### Lazy Initialization (Manual Control)
159+
160+
```csharp
161+
services.AddWeaviateLocal(
162+
hostname: "localhost",
163+
eagerInitialization: false // Opt-in to lazy initialization
164+
);
165+
```
166+
167+
**How it works:**
168+
- Client is created but NOT initialized
169+
- You must call `InitializeAsync()` before first use
170+
- Useful for scenarios where you want to control when initialization happens
171+
172+
```csharp
173+
public class MyService
174+
{
175+
private readonly WeaviateClient _client;
176+
177+
public MyService(WeaviateClient client)
178+
{
179+
// ⚠️ Client is NOT yet initialized
180+
_client = client;
181+
}
182+
183+
public async Task Initialize()
184+
{
185+
// ✅ Manually trigger initialization
186+
await _client.InitializeAsync();
187+
}
188+
189+
public async Task DoWork()
190+
{
191+
// Check if initialized
192+
if (!_client.IsInitialized)
193+
{
194+
await _client.InitializeAsync();
195+
}
196+
197+
var results = await _client.Collections.Get("Article").Query.FetchObjects();
198+
}
199+
}
200+
```
201+
202+
### Checking Initialization Status
203+
204+
Use the `IsInitialized` property to check if the client is ready:
205+
206+
```csharp
207+
if (client.IsInitialized)
208+
{
209+
// Safe to use RestClient and GrpcClient
210+
var results = await client.Collections.Get("Article").Query.FetchObjects();
211+
}
212+
else
213+
{
214+
// Must call InitializeAsync() first
215+
await client.InitializeAsync();
216+
}
217+
```
218+
219+
**What happens during initialization:**
220+
1. Token service is created (for authentication)
221+
2. REST client is configured
222+
3. Server metadata is fetched (validates auth, gets gRPC settings)
223+
4. gRPC client is configured with server metadata
224+
5. `RestClient` and `GrpcClient` properties are populated
225+
226+
### Important: When is the Client Ready?
227+
228+
| Pattern | When Ready | RestClient/GrpcClient Available |
229+
|---------|-----------|--------------------------------|
230+
| `await BuildAsync()` | Immediately after return | ✅ Yes |
231+
| DI Eager (default) | Before app starts serving | ✅ Yes |
232+
| DI Lazy | After calling `InitializeAsync()` | ⚠️ Only after init |
233+
234+
**⚠️ Using uninitialized client causes `NullReferenceException`:**
235+
236+
```csharp
237+
// ❌ BAD: Lazy DI without calling InitializeAsync()
238+
var client = serviceProvider.GetService<WeaviateClient>();
239+
var results = await client.Collections.Get("Article").Query.FetchObjects(); // NullReferenceException!
240+
241+
// ✅ GOOD: Check and initialize if needed
242+
var client = serviceProvider.GetService<WeaviateClient>();
243+
if (!client.IsInitialized)
244+
{
245+
await client.InitializeAsync();
246+
}
247+
var results = await client.Collections.Get("Article").Query.FetchObjects();
248+
```
249+
250+
### Automatic Initialization Guards
251+
252+
**✨ 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.
253+
254+
**How it works:**
255+
256+
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:
257+
258+
- **With eager initialization (default)**: The guard passes immediately since the client is already initialized
259+
- **With lazy initialization**: The guard triggers initialization automatically on first use
260+
261+
```csharp
262+
// Lazy DI - initialization happens automatically on first call
263+
services.AddWeaviateLocal(hostname: "localhost", eagerInitialization: false);
264+
265+
// Later in your code...
266+
var client = serviceProvider.GetService<WeaviateClient>();
267+
268+
// ✅ This works! The client auto-initializes on first use
269+
var collections = await client.Collections.List(); // Initialization happens here automatically
270+
```
271+
272+
**Performance Impact:**
273+
274+
- **Eager initialization (default)**: No overhead - guards pass through immediately
275+
- **Lazy initialization**: Minimal overhead - first call triggers initialization, subsequent calls pass through
276+
- Guards use `Lazy<Task>` internally, ensuring initialization happens exactly once even with concurrent calls
277+
278+
**When guards help:**
279+
280+
- Forgetting to call `InitializeAsync()` in lazy initialization scenarios
281+
- Race conditions where multiple threads access an uninitialized client
282+
- Unit tests that don't properly set up client initialization
283+
284+
**What's protected:**
285+
286+
-`client.Collections.*` - All collection operations
287+
-`client.Cluster.*` - All cluster operations
288+
-`client.Alias.*` - All alias operations
289+
-`client.Users.*`, `client.Roles.*`, `client.Groups.*` - All auth operations
290+
- ✅ Collection-level operations like `collection.Delete()`, `collection.Iterator()`
291+
- ✅ All async methods that access REST or gRPC clients
292+
293+
**What's not protected:**
294+
295+
- ❌ Synchronous property accessors (design limitation - properties can't be async)
296+
- ❌ Direct access to internal clients (not part of public API)
297+
103298
## Connection Configuration
104299

105300
### Local Development

src/Weaviate.Client/AliasClient.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal AliasClient(WeaviateClient client)
2222
/// <returns>The alias with its target collection</returns>
2323
public async Task<Alias?> Get(string aliasName, CancellationToken cancellationToken = default)
2424
{
25+
await _client.EnsureInitializedAsync();
2526
var dto = await _client.RestClient.AliasGet(aliasName, cancellationToken);
2627
return dto != null ? ToModel(dto) : null;
2728
}
@@ -34,6 +35,7 @@ internal AliasClient(WeaviateClient client)
3435
/// <returns>The created alias</returns>
3536
public async Task<Alias> Add(Alias alias, CancellationToken cancellationToken = default)
3637
{
38+
await _client.EnsureInitializedAsync();
3739
var dto = ToDto(alias);
3840
var result = await _client.RestClient.CollectionAliasesPost(dto, cancellationToken);
3941
return ToModel(result);
@@ -50,6 +52,7 @@ public async Task<IEnumerable<Alias>> List(
5052
CancellationToken cancellationToken = default
5153
)
5254
{
55+
await _client.EnsureInitializedAsync();
5356
var dtos = await _client.RestClient.CollectionAliasesGet(collectionName, cancellationToken);
5457
return dtos.Select(ToModel);
5558
}
@@ -67,6 +70,7 @@ public async Task<Alias> Update(
6770
CancellationToken cancellationToken = default
6871
)
6972
{
73+
await _client.EnsureInitializedAsync();
7074
var dto = await _client.RestClient.AliasPut(aliasName, targetCollection, cancellationToken);
7175
return ToModel(dto);
7276
}
@@ -78,6 +82,7 @@ public async Task<Alias> Update(
7882
/// <param name="cancellationToken">Cancellation token for the operation</param>
7983
public async Task<bool> Delete(string aliasName, CancellationToken cancellationToken = default)
8084
{
85+
await _client.EnsureInitializedAsync();
8186
return await _client.RestClient.AliasDelete(aliasName, cancellationToken);
8287
}
8388

src/Weaviate.Client/ClusterClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public async Task<ReplicationOperationTracker> Replicate(
3636
CancellationToken cancellationToken = default
3737
)
3838
{
39+
await _client.EnsureInitializedAsync();
3940
var dto = new Rest.Dto.ReplicationReplicateReplicaRequest
4041
{
4142
Collection = request.Collection,

src/Weaviate.Client/CollectionClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ internal CollectionClient(
6161
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
6262
public async Task Delete(CancellationToken cancellationToken = default)
6363
{
64+
await _client.EnsureInitializedAsync();
6465
await _client.RestClient.CollectionDelete(Name, cancellationToken);
6566
}
6667

@@ -74,6 +75,7 @@ public async IAsyncEnumerable<WeaviateObject> Iterator(
7475
[EnumeratorCancellation] CancellationToken cancellationToken = default
7576
)
7677
{
78+
await _client.EnsureInitializedAsync();
7779
Guid? cursor = after;
7880

7981
while (true)

src/Weaviate.Client/CollectionsClient.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public async Task<CollectionClient> Create(
1616
CancellationToken cancellationToken = default
1717
)
1818
{
19+
await _client.EnsureInitializedAsync();
1920
var response = await _client.RestClient.CollectionCreate(
2021
collection.ToDto(),
2122
cancellationToken
@@ -31,6 +32,7 @@ public async Task<CollectionClient> Create(
3132
)
3233
where T : class, new()
3334
{
35+
await _client.EnsureInitializedAsync();
3436
var response = await _client.RestClient.CollectionCreate(
3537
collection.ToDto(),
3638
cancellationToken
@@ -45,6 +47,7 @@ public async Task Delete(string collectionName, CancellationToken cancellationTo
4547
{
4648
ArgumentException.ThrowIfNullOrEmpty(collectionName);
4749

50+
await _client.EnsureInitializedAsync();
4851
await _client.RestClient.CollectionDelete(collectionName, cancellationToken);
4952
}
5053

@@ -62,6 +65,7 @@ public async Task<bool> Exists(
6265
CancellationToken cancellationToken = default
6366
)
6467
{
68+
await _client.EnsureInitializedAsync();
6569
return await _client.RestClient.CollectionExists(collectionName, cancellationToken);
6670
}
6771

@@ -70,6 +74,7 @@ public async Task<bool> Exists(
7074
CancellationToken cancellationToken = default
7175
)
7276
{
77+
await _client.EnsureInitializedAsync();
7378
var response = await _client.RestClient.CollectionGet(collectionName, cancellationToken);
7479

7580
if (response is null)
@@ -85,6 +90,7 @@ public async Task<bool> Exists(
8590
CancellationToken cancellationToken = default
8691
)
8792
{
93+
await _client.EnsureInitializedAsync();
8894
var response = await _client.RestClient.CollectionList(cancellationToken);
8995

9096
foreach (var c in response?.Classes ?? Enumerable.Empty<Rest.Dto.Class>())

src/Weaviate.Client/RolesClient.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ public class RolesClient
1313

1414
public async Task<IEnumerable<RoleInfo>> ListAll(CancellationToken cancellationToken = default)
1515
{
16+
await _client.EnsureInitializedAsync();
1617
var roles = await _client.RestClient.RolesList(cancellationToken);
1718
return roles.Select(r => r.ToModel());
1819
}
1920

2021
public async Task<RoleInfo?> Get(string id, CancellationToken cancellationToken = default)
2122
{
23+
await _client.EnsureInitializedAsync();
2224
var role = await _client.RestClient.RoleGet(id, cancellationToken);
2325
return role is null ? null : role.ToModel();
2426
}
@@ -29,6 +31,7 @@ public async Task<RoleInfo> Create(
2931
CancellationToken cancellationToken = default
3032
)
3133
{
34+
await _client.EnsureInitializedAsync();
3235
var dto = new Rest.Dto.Role
3336
{
3437
Name = name,
@@ -38,15 +41,19 @@ public async Task<RoleInfo> Create(
3841
return created.ToModel();
3942
}
4043

41-
public Task Delete(string id, CancellationToken cancellationToken = default) =>
42-
_client.RestClient.RoleDelete(id, cancellationToken);
44+
public async Task Delete(string id, CancellationToken cancellationToken = default)
45+
{
46+
await _client.EnsureInitializedAsync();
47+
await _client.RestClient.RoleDelete(id, cancellationToken);
48+
}
4349

4450
public async Task<RoleInfo> AddPermissions(
4551
string id,
4652
IEnumerable<PermissionScope> permissions,
4753
CancellationToken cancellationToken = default
4854
)
4955
{
56+
await _client.EnsureInitializedAsync();
5057
var dtos = permissions.SelectMany(p => p.ToDto()).ToList();
5158
var updated = await _client.RestClient.RoleAddPermissions(id, dtos, cancellationToken);
5259
return updated.ToModel();
@@ -58,6 +65,7 @@ public async Task<RoleInfo> RemovePermissions(
5865
CancellationToken cancellationToken = default
5966
)
6067
{
68+
await _client.EnsureInitializedAsync();
6169
var dtos = permissions.SelectMany(p => p.ToDto()).ToList();
6270
var updated = await _client.RestClient.RoleRemovePermissions(id, dtos, cancellationToken);
6371
return updated.ToModel();
@@ -69,6 +77,7 @@ public async Task<bool> HasPermission(
6977
CancellationToken cancellationToken = default
7078
)
7179
{
80+
await _client.EnsureInitializedAsync();
7281
var dto = permission.ToDto().Single();
7382
return await _client.RestClient.RoleHasPermission(id, dto, cancellationToken);
7483
}
@@ -78,6 +87,7 @@ public async Task<IEnumerable<UserRoleAssignment>> GetUserAssignments(
7887
CancellationToken cancellationToken = default
7988
)
8089
{
90+
await _client.EnsureInitializedAsync();
8191
var list = await _client.RestClient.RoleUserAssignments(roleId, cancellationToken);
8292
return list.Select(a => a.ToModel());
8393
}
@@ -87,6 +97,7 @@ public async Task<IEnumerable<GroupRoleAssignment>> GetGroupAssignments(
8797
CancellationToken cancellationToken = default
8898
)
8999
{
100+
await _client.EnsureInitializedAsync();
90101
var list = await _client.RestClient.RoleGroupAssignments(roleId, cancellationToken);
91102
return list.Select(a => a.ToModel());
92103
}

src/Weaviate.Client/UsersClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ internal UsersClient(WeaviateClient client)
3232
/// </summary>
3333
public async Task<CurrentUserInfo?> OwnInfo(CancellationToken cancellationToken = default)
3434
{
35+
await _client.EnsureInitializedAsync();
3536
var dto = await _client.RestClient.UserOwnInfoGet(cancellationToken);
3637
if (dto is null)
3738
return null;

0 commit comments

Comments
 (0)