A provider-based data store abstraction with explicit startup initialization, connection-scoped provider instances, named serializers, and Redis support.
| Build | |
| DataStore | |
| DataStore.Redis | |
| DataStore.Serializer.JsonNet | |
| DataStore.Serializer.Ceras |
dotnet add package Nuve.DataStore --version 2.0.7
dotnet add package Nuve.DataStore.Redis --version 2.0.7
dotnet add package Nuve.DataStore.Serializer.JsonNet --version 2.0.7Add Nuve.DataStore.Serializer.Ceras only if you need the Ceras serializer.
- Providers are registered from code only.
- Connections can come from configuration, code, or both.
- Each connection owns its own
ConnectionOptionsand initializes its own provider instance. - Stores are still created with
new; they are not resolved from DI inv2.0.x. InitializeDataStore()orInitializeDataStoreAsync()is mandatory before any store is created or used.
using Nuve.DataStore;
using Nuve.DataStore.Redis;
using Nuve.DataStore.Serializer.JsonNet;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddDataStore()
.AddDataStoreSerializer<JsonNetDataStoreSerializer>()
.AddRedisDataStoreProvider()
.AddDefaultConnection(
provider: "redis",
options: new ConnectionOptions
{
ConnectionString = "localhost:6379,abortConnect=false",
ConnectionMode = ConnectionMode.Shared
},
rootNamespace: "app");
var app = builder.Build();
await app.Services.InitializeDataStoreAsync();
var store = new KeyValueStore();
await store.SetAsync("key", "value");
var value = await store.GetAsync<string>("key");AddDataStore() can be used in two ways:
builder.Services.AddDataStore();This uses the application's registered IConfiguration when DataStoreManager is created.
builder.Services.AddDataStore(builder.Configuration);This uses the explicitly supplied configuration instead.
Configuration precedence is:
- Configuration sources are merged first.
- Code registrations are applied after configuration.
ConnectionOptionsoverload replaces options completely.Action<ConnectionOptions>overload mutates the existing configuration-based options.
On net48, legacy web.config / app.config fallback is also supported when IConfiguration is not used.
Connections are configured as a keyed object, not an array. This allows later configuration providers to override only selected fields.
{
"DataStore": {
"Connections": {
"Default": {
"IsDefault": true,
"Provider": "redis",
"Serializer": "json",
"ConnectionString": "localhost:6379,abortConnect=false",
"ConnectionMode": "Shared",
"RetryCount": 5,
"MaxPoolSize": 8,
"PoolWaitTimeout": "00:00:02",
"BackgroundProbeMinInterval": "00:00:05",
"HealthCheckTimeout": "00:00:02",
"SwapDisposeDelay": "00:00:05",
"RootNamespace": "app"
},
"cache": {
"Provider": "redis",
"Serializer": "ceras",
"ConnectionString": "localhost:6379,abortConnect=false",
"ConnectionMode": "Pooled",
"MaxPoolSize": 16,
"RootNamespace": "cache"
}
}
}
}Partial override example:
{
"DataStore": {
"Connections": {
"Default": {
"RootNamespace": "wcf"
}
}
}
}Required connection fields:
ProviderConnectionString
Optional connection fields:
SerializerConnectionModeRetryCountMaxPoolSizePoolWaitTimeoutBackgroundProbeMinIntervalHealthCheckTimeoutSwapDisposeDelayRootNamespaceCompressBiggerThanIsDefault
Providers are added from code and resolved by name with StringComparer.OrdinalIgnoreCase.
builder.Services
.AddDataStore()
.AddRedisDataStoreProvider(); // default provider name: "redis"
builder.Services
.AddDataStore()
.AddRedisDataStoreProvider("cache"); // custom provider nameIf the same provider name is registered more than once, the first registration is kept and a warning is logged.
builder.Services
.AddDataStore()
.AddRedisDataStoreProvider()
.AddDefaultConnection(
provider: "redis",
options: new ConnectionOptions
{
ConnectionString = "localhost:6379,abortConnect=false",
ConnectionMode = ConnectionMode.Shared
},
rootNamespace: "app");builder.Services
.AddDataStore()
.AddRedisDataStoreProvider()
.AddConnection(
name: "cache",
provider: "redis",
options: new ConnectionOptions
{
ConnectionString = "localhost:6380,abortConnect=false",
ConnectionMode = ConnectionMode.Pooled,
MaxPoolSize = 16
},
rootNamespace: "cache");Use the named connection from a store:
var defaultStore = new KeyValueStore();
var cacheStore = new KeyValueStore("cache");Use the ConnectionOptions overload when code should replace the connection options completely:
builder.Services
.AddDataStore(builder.Configuration)
.AddRedisDataStoreProvider()
.AddConnection(
name: "cache",
provider: "redis",
options: new ConnectionOptions
{
ConnectionString = "localhost:6379,abortConnect=false",
ConnectionMode = ConnectionMode.Pooled,
MaxPoolSize = 32
},
serializer: "ceras",
rootNamespace: "cache");Use the Action<ConnectionOptions> overload when configuration should be the base and code should only modify selected fields:
builder.Services
.AddDataStore(builder.Configuration)
.AddRedisDataStoreProvider()
.AddConnection(
name: "cache",
provider: "redis",
configure: options =>
{
options.RetryCount = 10;
options.MaxPoolSize = 32;
},
serializer: "ceras");AddDataStoreSerializer(...) without a name sets the default serializer.
builder.Services
.AddDataStore()
.AddDataStoreSerializer<JsonNetDataStoreSerializer>();Named serializers can be registered and selected per connection:
builder.Services
.AddDataStore()
.AddDataStoreSerializer<JsonNetDataStoreSerializer>("json")
.AddDataStoreSerializer<CerasDataStoreSerializer>("ceras")
.AddRedisDataStoreProvider()
.AddDefaultConnection(
provider: "redis",
serializer: "json",
options: new ConnectionOptions
{
ConnectionString = "localhost:6379,abortConnect=false"
})
.AddConnection(
name: "binary-cache",
provider: "redis",
serializer: "ceras",
options: new ConnectionOptions
{
ConnectionString = "localhost:6379,abortConnect=false"
});If a connection does not specify Serializer, the default serializer is used.
Provider initialization is explicit and eager. It is not lazy.
await serviceProvider.InitializeDataStoreAsync();
// or
serviceProvider.InitializeDataStore();What happens during initialization:
- All finalized connections are resolved.
- Each connection creates its own provider instance.
- Each provider is initialized with that connection's own
ConnectionOptions. - After initialization, store creation and provider lookup stay on the runtime fast path.
If a connection references an unknown provider or serializer, initialization fails immediately.
var store = new KeyValueStore();
await store.SetAsync("user:1", "John");
var value = await store.GetAsync<string>("user:1");var store = new DictionaryStore();
await store.SetAsync("user:1", "name", "John");
await store.SetAsync("user:1", "age", 30);
var name = await store.GetAsync<string>("user:1", "name");var store = new HashStore();
await store.SetAsync("user:1", "name", "John");
await store.SetAsync("user:1", "age", 30);
var all = await store.GetAllAsync("user:1");var store = new HashSetStore();
await store.AddAsync("tags", "redis");
await store.AddAsync("tags", "cache");
var exists = await store.ContainsAsync("tags", "redis");var store = new LinkedListStore<string>();
await store.AddLastAsync("queue", "job1");
await store.AddLastAsync("queue", "job2");var store = new KeyValueStore();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var lockItem = store.AcquireLock(
"resource-lock",
waitCancelToken: cts.Token,
slidingExpire: TimeSpan.FromSeconds(10),
throwWhenTimeout: true);
if (lockItem is not null)
{
// critical section
}await store.LockAsync(
"resource-lock",
waitTimeout: TimeSpan.FromSeconds(5),
actionAsync: async () =>
{
await Task.CompletedTask;
},
slidingExpire: TimeSpan.FromSeconds(10),
throwWhenTimeout: true);- Store instances are created with
new. - Do not create or use stores before
InitializeDataStore()orInitializeDataStoreAsync(). - Provider names are case-insensitive.
- Serializer names are case-insensitive.
- Duplicate connection names are resolved by the last registration.
- With
Action<ConnectionOptions>, existing configuration values are preserved unless code changes them. - With
ConnectionOptions, the connection options are replaced completely.
Tests use the REDIS_TEST_CONNECTION environment variable. The repository includes test.runsettings.
<RunSettings>
<RunConfiguration>
<EnvironmentVariables>
<REDIS_TEST_CONNECTION>localhost:6379,abortConnect=false</REDIS_TEST_CONNECTION>
</EnvironmentVariables>
</RunConfiguration>
</RunSettings>dotnet test --settings test.runsettings- .NET Standard 2.1
- .NET Framework 4.8
- .NET 6
- .NET 7
- .NET 8
- .NET 9
- .NET 10