Add ergonomic CultLib happy-path APIs
This commit is contained in:
parent
3f1cb8b36e
commit
80d0485d3c
9 changed files with 471 additions and 7 deletions
90
src/GameCult.Caching.MessagePack/CultCacheMessagePack.cs
Normal file
90
src/GameCult.Caching.MessagePack/CultCacheMessagePack.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GameCult.Caching.MessagePack
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for opening a CultCache over the canonical single-file MessagePack backing store.
|
||||
/// </summary>
|
||||
public sealed class CultCacheOpenOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the document registry to use. When omitted, the shared registry is used.
|
||||
/// </summary>
|
||||
public CultDocumentRegistry? Registry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the cache should pull the existing snapshot during open.
|
||||
/// </summary>
|
||||
public bool PullOnOpen { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the cache should flush attached stores when disposed.
|
||||
/// </summary>
|
||||
public bool FlushOnDispose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the backing store should flush when disposed.
|
||||
/// </summary>
|
||||
public bool StoreFlushOnDispose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional callback used to customize the cache before opening.
|
||||
/// </summary>
|
||||
public Action<CultCache>? ConfigureCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional callback used to customize the backing store before opening.
|
||||
/// </summary>
|
||||
public Action<SingleFileMessagePackBackingStore>? ConfigureStore { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Friendly MessagePack entrypoints for opening a CultCache with the canonical backing store.
|
||||
/// </summary>
|
||||
public static class CultCacheMessagePack
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a cache with the canonical single-file MessagePack backing store attached.
|
||||
/// </summary>
|
||||
public static CultCache Create(string filePath, CultCacheOpenOptions? options = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
throw new ArgumentException("File path must be non-empty.", nameof(filePath));
|
||||
}
|
||||
|
||||
options ??= new CultCacheOpenOptions();
|
||||
|
||||
var cache = new CultCache(options.Registry)
|
||||
{
|
||||
FlushAttachedStoresOnDispose = options.FlushOnDispose
|
||||
};
|
||||
options.ConfigureCache?.Invoke(cache);
|
||||
|
||||
var store = new SingleFileMessagePackBackingStore(filePath)
|
||||
{
|
||||
FlushOnDispose = options.StoreFlushOnDispose
|
||||
};
|
||||
options.ConfigureStore?.Invoke(store);
|
||||
|
||||
cache.AddBackingStore(store);
|
||||
return cache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache with the canonical single-file MessagePack backing store attached and optionally pulls the on-disk snapshot.
|
||||
/// </summary>
|
||||
public static async Task<CultCache> OpenAsync(string filePath, CultCacheOpenOptions? options = null)
|
||||
{
|
||||
options ??= new CultCacheOpenOptions();
|
||||
var cache = Create(filePath, options);
|
||||
if (options.PullOnOpen)
|
||||
{
|
||||
await cache.PullAllBackingStoresAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1266,6 +1266,15 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes all attached backing stores.
|
||||
/// </summary>
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
FlushAllBackingStores(soft);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes one attached backing store.
|
||||
/// </summary>
|
||||
|
|
@ -1293,6 +1302,15 @@ namespace GameCult.Caching
|
|||
FlushAllBackingStores(soft);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes attached backing stores at a lifecycle boundary such as shutdown or assembly reload.
|
||||
/// </summary>
|
||||
public Task PrepareForReloadOrShutdownAsync(bool soft = false)
|
||||
{
|
||||
PrepareForReloadOrShutdown(soft);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or replaces a typed document and returns its record handle.
|
||||
/// </summary>
|
||||
|
|
@ -1306,6 +1324,14 @@ namespace GameCult.Caching
|
|||
return new CultRecordHandle<T>(stored.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or replaces a typed document and returns its record handle.
|
||||
/// </summary>
|
||||
public Task<CultRecordHandle<T>> UpsertAsync<T>(T document, CultRecordHandle<T>? handle = null)
|
||||
{
|
||||
return AddAsync(document, handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the record handle for a document instance, if it is tracked.
|
||||
/// </summary>
|
||||
|
|
@ -1335,6 +1361,15 @@ namespace GameCult.Caching
|
|||
return Get(key) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a typed document by record key.
|
||||
/// </summary>
|
||||
public bool TryGet<T>(CultRecordKey key, out T? document) where T : class
|
||||
{
|
||||
document = Get<T>(key);
|
||||
return document != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all cached documents assignable to the requested type.
|
||||
/// </summary>
|
||||
|
|
@ -1371,6 +1406,15 @@ namespace GameCult.Caching
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a typed document by its CultName value.
|
||||
/// </summary>
|
||||
public bool TryGetByName<T>(string name, out T? document) where T : class
|
||||
{
|
||||
document = GetByName<T>(name);
|
||||
return document != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a typed document by an indexed value.
|
||||
/// </summary>
|
||||
|
|
@ -1385,6 +1429,15 @@ namespace GameCult.Caching
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a typed document by an indexed value.
|
||||
/// </summary>
|
||||
public bool TryGetByIndex<T>(string alias, string value, out T? document) where T : class
|
||||
{
|
||||
document = GetByIndex<T>(alias, value);
|
||||
return document != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a typed document reference against this cache.
|
||||
/// </summary>
|
||||
|
|
@ -1404,6 +1457,15 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a document by typed handle.
|
||||
/// </summary>
|
||||
public Task DeleteAsync<T>(CultRecordHandle<T> handle)
|
||||
{
|
||||
Remove(handle);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes attached disposable backing stores.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -121,19 +121,28 @@ Each persisted record carries:
|
|||
- `storedAt`
|
||||
- `payload`
|
||||
|
||||
## Typical Startup
|
||||
## Happy Path
|
||||
|
||||
```csharp
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
||||
var cache = new CultCache();
|
||||
var store = new SingleFileMessagePackBackingStore("Data.msgpack");
|
||||
var cache = await CultCacheMessagePack.OpenAsync("Data.msgpack");
|
||||
|
||||
cache.AddBackingStore(store);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var handle = await cache.UpsertAsync(new ItemData
|
||||
{
|
||||
Name = "Potion",
|
||||
Category = "Consumable",
|
||||
Value = 50
|
||||
});
|
||||
|
||||
await cache.FlushAsync();
|
||||
```
|
||||
|
||||
If you want the explicit assembly rite, the lower-level `CultCache` +
|
||||
`SingleFileMessagePackBackingStore` path is still there. The helper is just the
|
||||
blessed front door.
|
||||
|
||||
## Persistence discipline
|
||||
|
||||
Persistence stays manual by default. Mutations mark the cache and attached
|
||||
|
|
@ -142,11 +151,15 @@ that felt convenient in the moment.
|
|||
|
||||
Useful surfaces:
|
||||
|
||||
- `CultCacheMessagePack.OpenAsync(path)`
|
||||
- `cache.IsDirty`
|
||||
- `store.IsDirty`
|
||||
- `cache.UpsertAsync(document)`
|
||||
- `cache.FlushAllBackingStores()`
|
||||
- `cache.FlushAsync()`
|
||||
- `cache.FlushBackingStore(store)`
|
||||
- `cache.PrepareForReloadOrShutdown()`
|
||||
- `cache.PrepareForReloadOrShutdownAsync()`
|
||||
- `cache.FlushAttachedStoresOnDispose`
|
||||
- `store.FlushOnDispose`
|
||||
|
||||
|
|
|
|||
|
|
@ -164,6 +164,14 @@ namespace GameCult.Networking
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a specific message type.
|
||||
/// </summary>
|
||||
public void On<T>(Action<T> callback) where T : Message
|
||||
{
|
||||
AddMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered listener for a specific message type.
|
||||
/// </summary>
|
||||
|
|
@ -178,6 +186,14 @@ namespace GameCult.Networking
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered listener for a specific message type.
|
||||
/// </summary>
|
||||
public void Off<T>(Action<T> callback) where T : Message
|
||||
{
|
||||
RemoveMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a login request.
|
||||
/// </summary>
|
||||
|
|
|
|||
162
src/GameCult.Networking/CultNetLocal.cs
Normal file
162
src/GameCult.Networking/CultNetLocal.cs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
||||
namespace GameCult.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for creating a local CultNet host over CultCache and the canonical MessagePack store.
|
||||
/// </summary>
|
||||
public sealed class CultNetHostOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cache-open options used for the underlying CultCache.
|
||||
/// </summary>
|
||||
public CultCacheOpenOptions? CacheOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server security configuration. When omitted, development defaults are used.
|
||||
/// </summary>
|
||||
public ServerSecurityOptions? Security { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the server should start immediately after creation.
|
||||
/// </summary>
|
||||
public bool StartServer { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional callback used to customize the server before start.
|
||||
/// </summary>
|
||||
public Action<Server>? ConfigureServer { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience wrapper for a locally hosted CultNet server over a durable CultCache.
|
||||
/// </summary>
|
||||
public sealed class CultNetHost : IDisposable
|
||||
{
|
||||
internal CultNetHost(CultCache cache, SingleFileMessagePackBackingStore? store, Server server)
|
||||
{
|
||||
Cache = cache;
|
||||
Store = store;
|
||||
Server = server;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the durable cache used by the host.
|
||||
/// </summary>
|
||||
public CultCache Cache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the canonical single-file MessagePack store, when one is attached.
|
||||
/// </summary>
|
||||
public SingleFileMessagePackBackingStore? Store { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying CultNet server.
|
||||
/// </summary>
|
||||
public Server Server { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the server.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
Server.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the durable cache.
|
||||
/// </summary>
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
return Cache.FlushAsync(soft);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the server and disposes the host resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Server.Dispose();
|
||||
Cache.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Friendly local entrypoints for CultNet host/client creation.
|
||||
/// </summary>
|
||||
public static class CultNetLocal
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a local host with a durable CultCache and the canonical MessagePack backing store.
|
||||
/// </summary>
|
||||
public static async Task<CultNetHost> CreateHostAsync(string cachePath, CultNetHostOptions? options = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cachePath))
|
||||
{
|
||||
throw new ArgumentException("Cache path must be non-empty.", nameof(cachePath));
|
||||
}
|
||||
|
||||
options ??= new CultNetHostOptions();
|
||||
var cache = await CultCacheMessagePack.OpenAsync(cachePath, options.CacheOptions).ConfigureAwait(false);
|
||||
var store = cache.BackingStores.OfType<SingleFileMessagePackBackingStore>().FirstOrDefault();
|
||||
var server = new Server(cache, options.Security ?? ServerSecurityOptions.Development());
|
||||
options.ConfigureServer?.Invoke(server);
|
||||
|
||||
var host = new CultNetHost(cache, store, server);
|
||||
if (options.StartServer)
|
||||
{
|
||||
host.Start();
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and starts a local host with development-friendly defaults.
|
||||
/// </summary>
|
||||
public static Task<CultNetHost> StartHostAsync(string cachePath, CultNetHostOptions? options = null)
|
||||
{
|
||||
options ??= new CultNetHostOptions();
|
||||
options.StartServer = true;
|
||||
return CreateHostAsync(cachePath, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a client configured for local CultNet use.
|
||||
/// </summary>
|
||||
public static Client CreateClient(ClientSecurityOptions? security = null, Action<Client>? configureClient = null)
|
||||
{
|
||||
var client = new Client(security ?? ClientSecurityOptions.Development());
|
||||
configureClient?.Invoke(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and connects a client to the specified host.
|
||||
/// </summary>
|
||||
public static Client ConnectClient(
|
||||
string host = "localhost",
|
||||
int port = 3075,
|
||||
ClientSecurityOptions? security = null,
|
||||
Action<Client>? configureClient = null)
|
||||
{
|
||||
var client = CreateClient(security, configureClient);
|
||||
client.Connect(host, port);
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and connects a client to a local development host.
|
||||
/// </summary>
|
||||
public static Client ConnectLocal(
|
||||
ClientSecurityOptions? security = null,
|
||||
Action<Client>? configureClient = null)
|
||||
{
|
||||
return ConnectClient("localhost", 3075, security, configureClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -183,6 +183,20 @@ Use `ServerSecurityOptions.Development()` only for local development and tests.
|
|||
|
||||
## Basic Server Usage
|
||||
|
||||
Quickest local-host path:
|
||||
|
||||
```csharp
|
||||
using GameCult.Networking;
|
||||
|
||||
var host = await CultNetLocal.StartHostAsync("Players.msgpack");
|
||||
host.Server.On<ChatMessage>(message =>
|
||||
{
|
||||
host.Server.Logger.LogInfo($"Chat: {message.Text}");
|
||||
});
|
||||
```
|
||||
|
||||
Lower-level explicit path:
|
||||
|
||||
```csharp
|
||||
using GameCult.Caching;
|
||||
using GameCult.Logging;
|
||||
|
|
@ -194,7 +208,7 @@ var server = new Server(cache)
|
|||
Logger = new ConsoleLogger()
|
||||
};
|
||||
|
||||
server.AddMessageListener<ChatMessage>(message =>
|
||||
server.On<ChatMessage>(message =>
|
||||
{
|
||||
server.Logger.LogInfo($"Chat: {message.Text}");
|
||||
});
|
||||
|
|
@ -204,6 +218,18 @@ server.Start();
|
|||
|
||||
## Basic Client Usage
|
||||
|
||||
Quickest local-client path:
|
||||
|
||||
```csharp
|
||||
using GameCult.Networking;
|
||||
|
||||
var client = CultNetLocal.ConnectLocal();
|
||||
client.On<ChatMessage>(message => Console.WriteLine(message.Text));
|
||||
client.Login("user@example.com", "correct horse battery staple");
|
||||
```
|
||||
|
||||
Lower-level explicit path:
|
||||
|
||||
```csharp
|
||||
using GameCult.Logging;
|
||||
using GameCult.Networking;
|
||||
|
|
@ -214,7 +240,7 @@ var client = new Client(new ClientSecurityOptions("<matching-connection-key>"))
|
|||
};
|
||||
|
||||
client.OnError += error => client.Logger.LogError($"Client error: {error}");
|
||||
client.AddMessageListener<ChatMessage>(message => client.Logger.LogInfo($"Chat: {message.Text}"));
|
||||
client.On<ChatMessage>(message => client.Logger.LogInfo($"Chat: {message.Text}"));
|
||||
|
||||
client.Connect("localhost", 3075);
|
||||
client.Login("user@example.com", "correct horse battery staple");
|
||||
|
|
|
|||
|
|
@ -111,6 +111,14 @@ namespace GameCult.Networking
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a specific authenticated message type.
|
||||
/// </summary>
|
||||
public void On<T>(Action<T> callback) where T : Message
|
||||
{
|
||||
AddMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered listener for a specific message type.
|
||||
/// </summary>
|
||||
|
|
@ -125,6 +133,14 @@ namespace GameCult.Networking
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered listener for a specific authenticated message type.
|
||||
/// </summary>
|
||||
public void Off<T>(Action<T> callback) where T : Message
|
||||
{
|
||||
RemoveMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the underlying LiteNetLib server.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -92,6 +93,35 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCacheMessagePack_OpenAsync_Creates_Usable_Durable_Cache()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.msgpack");
|
||||
|
||||
try
|
||||
{
|
||||
var cache = await CultCacheMessagePack.OpenAsync(filePath);
|
||||
var handle = await cache.UpsertAsync(new NamedTestEntry
|
||||
{
|
||||
Name = "open",
|
||||
Value = "magic"
|
||||
});
|
||||
await cache.FlushAsync();
|
||||
cache.Dispose();
|
||||
|
||||
var reopened = await CultCacheMessagePack.OpenAsync(filePath);
|
||||
Assert.That(reopened.TryGet(handle.Key, out NamedTestEntry? loaded), Is.True);
|
||||
Assert.That(loaded!.Value, Is.EqualTo("magic"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_FlushOnDispose_Persists_When_Enabled()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -334,6 +335,54 @@ namespace GameCult.Networking.Tests
|
|||
Assert.That(developmentServer.LogSensitivePayloads, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultNetLocal_CreateHostAsync_Wires_Durable_Cache_Without_Starting_Server()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"cultnet-host-{Guid.NewGuid():N}.msgpack");
|
||||
|
||||
try
|
||||
{
|
||||
var host = await CultNetLocal.CreateHostAsync(filePath, new CultNetHostOptions
|
||||
{
|
||||
StartServer = false
|
||||
});
|
||||
|
||||
Assert.That(host.Cache, Is.Not.Null);
|
||||
Assert.That(host.Store, Is.Not.Null);
|
||||
Assert.That(host.Server, Is.Not.Null);
|
||||
|
||||
await host.Cache.UpsertAsync(new PlayerData
|
||||
{
|
||||
PlayerId = Guid.NewGuid(),
|
||||
Email = "host@example.test",
|
||||
PasswordHash = "hash",
|
||||
Username = "HostUser"
|
||||
});
|
||||
await host.FlushAsync();
|
||||
host.Dispose();
|
||||
|
||||
var reopened = await CultCacheMessagePack.OpenAsync(filePath);
|
||||
Assert.That(reopened.GetByName<PlayerData>("HostUser"), Is.Not.Null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CultNetLocal_CreateClient_Uses_Development_Defaults()
|
||||
{
|
||||
var client = CultNetLocal.CreateClient();
|
||||
|
||||
Assert.That(client, Is.Not.Null);
|
||||
Assert.That(client.LogSensitivePayloads, Is.False);
|
||||
Assert.That(client.ReconnectState, Is.EqualTo(ClientReconnectState.Idle));
|
||||
}
|
||||
|
||||
private sealed class EnvironmentVariableScope : IDisposable
|
||||
{
|
||||
private readonly (string Name, string? Value)[] _originalValues;
|
||||
|
|
|
|||
Loading…
Reference in a new issue