diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs
index f0910ad0b..428419710 100644
--- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs
+++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs
@@ -34,6 +34,7 @@ public static class DaprConfigurationStoreExtension
/// The used for the request.
/// The used to configure the timeout waiting for Dapr.
/// Optional metadata sent to the configuration store.
+ /// When true, does not block startup waiting for the sidecar. Configuration is loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddDaprConfigurationStore(
this IConfigurationBuilder configurationBuilder,
@@ -41,7 +42,8 @@ public static IConfigurationBuilder AddDaprConfigurationStore(
IReadOnlyList keys,
DaprClient client,
TimeSpan sidecarWaitTimeout,
- IReadOnlyDictionary? metadata = default)
+ IReadOnlyDictionary? metadata = default,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(keys, nameof(keys));
@@ -54,7 +56,8 @@ public static IConfigurationBuilder AddDaprConfigurationStore(
Client = client,
SidecarWaitTimeout = sidecarWaitTimeout,
IsStreaming = false,
- Metadata = metadata
+ Metadata = metadata,
+ IsOptional = optional
});
return configurationBuilder;
@@ -71,6 +74,7 @@ public static IConfigurationBuilder AddDaprConfigurationStore(
/// The used for the request.
/// The used to configure the timeout waiting for Dapr.
/// Optional metadata sent to the configuration store.
+ /// When true, does not block startup waiting for the sidecar. Configuration is loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddStreamingDaprConfigurationStore(
this IConfigurationBuilder configurationBuilder,
@@ -78,7 +82,8 @@ public static IConfigurationBuilder AddStreamingDaprConfigurationStore(
IReadOnlyList keys,
DaprClient client,
TimeSpan sidecarWaitTimeout,
- IReadOnlyDictionary? metadata = default)
+ IReadOnlyDictionary? metadata = default,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(keys, nameof(keys));
@@ -91,7 +96,8 @@ public static IConfigurationBuilder AddStreamingDaprConfigurationStore(
Client = client,
SidecarWaitTimeout = sidecarWaitTimeout,
IsStreaming = true,
- Metadata = metadata
+ Metadata = metadata,
+ IsOptional = optional
});
return configurationBuilder;
diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs
index 3e859b8d0..d32afd84b 100644
--- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs
+++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs
@@ -26,13 +26,14 @@ namespace Dapr.Extensions.Configuration;
///
internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposable
{
- private string store;
- private IReadOnlyList keys;
- private DaprClient daprClient;
- private TimeSpan sidecarWaitTimeout;
- private bool isStreaming;
- private IReadOnlyDictionary? metadata;
- private CancellationTokenSource cts;
+ private readonly string store;
+ private readonly IReadOnlyList keys;
+ private readonly DaprClient daprClient;
+ private readonly TimeSpan sidecarWaitTimeout;
+ private readonly bool isStreaming;
+ private readonly bool isOptional;
+ private readonly IReadOnlyDictionary? metadata;
+ private readonly CancellationTokenSource cts;
private Task subscribeTask = Task.CompletedTask;
///
@@ -44,19 +45,22 @@ internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposab
/// The used to configure the timeout waiting for Dapr.
/// Determines if the source is streaming or not.
/// Optional metadata sent to the configuration store.
+ /// When true, does not block startup waiting for the sidecar.
public DaprConfigurationStoreProvider(
string store,
IReadOnlyList keys,
DaprClient daprClient,
TimeSpan sidecarWaitTimeout,
bool isStreaming = false,
- IReadOnlyDictionary? metadata = default)
+ IReadOnlyDictionary? metadata = default,
+ bool isOptional = false)
{
this.store = store;
this.keys = keys;
this.daprClient = daprClient;
this.sidecarWaitTimeout = sidecarWaitTimeout;
this.isStreaming = isStreaming;
+ this.isOptional = isOptional;
this.metadata = metadata ?? new Dictionary();
this.cts = new CancellationTokenSource();
}
@@ -64,10 +68,60 @@ public DaprConfigurationStoreProvider(
public void Dispose()
{
cts.Cancel();
+ cts.Dispose();
}
///
- public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+ public override void Load()
+ {
+ if (isOptional)
+ {
+ Data = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _ = Task.Run(() => LoadInBackgroundAsync());
+ }
+ else
+ {
+ LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+ }
+
+ private async Task LoadInBackgroundAsync()
+ {
+ while (!cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ using var tokenSource = new CancellationTokenSource(sidecarWaitTimeout);
+ using var linked = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token, cts.Token);
+ await daprClient.WaitForSidecarAsync(linked.Token);
+
+ await FetchDataAsync();
+ OnReload();
+ return;
+ }
+ catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
+ {
+ return;
+ }
+ catch (OperationCanceledException)
+ {
+ // Sidecar wait timed out — retry after delay.
+ }
+ catch (DaprException)
+ {
+ // Transient Dapr error — retry after delay.
+ }
+
+ try
+ {
+ await Task.Delay(sidecarWaitTimeout, cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+ }
private async Task LoadAsync()
{
@@ -77,6 +131,11 @@ private async Task LoadAsync()
await daprClient.WaitForSidecarAsync(tokenSource.Token);
}
+ await FetchDataAsync();
+ }
+
+ private async Task FetchDataAsync()
+ {
if (isStreaming)
{
subscribeTask = Task.Run(async () =>
diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs
index 513e156b0..86012af5e 100644
--- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs
+++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs
@@ -53,9 +53,16 @@ public class DaprConfigurationStoreSource : IConfigurationSource
///
public IReadOnlyDictionary? Metadata { get; set; } = default;
+ ///
+ /// Gets or sets a value indicating whether this configuration source is optional.
+ /// When true, the provider will not block startup waiting for the Dapr sidecar and will
+ /// instead load configuration in the background once the sidecar becomes available.
+ ///
+ public bool IsOptional { get; set; }
+
///
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
- return new DaprConfigurationStoreProvider(Store, Keys, Client, SidecarWaitTimeout, IsStreaming, Metadata);
+ return new DaprConfigurationStoreProvider(Store, Keys, Client, SidecarWaitTimeout, IsStreaming, Metadata, IsOptional);
}
-}
\ No newline at end of file
+}
diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs
index 7df531e3e..0662ebe3e 100644
--- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs
+++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs
@@ -32,12 +32,14 @@ public static class DaprSecretStoreConfigurationExtensions
/// Dapr secret store name.
/// The secrets to retrieve.
/// The Dapr client
+ /// When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddDaprSecretStore(
this IConfigurationBuilder configurationBuilder,
string store,
IEnumerable secretDescriptors,
- DaprClient client)
+ DaprClient client,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
@@ -47,7 +49,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
{
Store = store,
SecretDescriptors = secretDescriptors,
- Client = client
+ Client = client,
+ IsOptional = optional
});
return configurationBuilder;
@@ -61,13 +64,15 @@ public static IConfigurationBuilder AddDaprSecretStore(
/// The secrets to retrieve.
/// The Dapr client.
/// The used to configure the timeout waiting for Dapr.
+ /// When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddDaprSecretStore(
this IConfigurationBuilder configurationBuilder,
string store,
IEnumerable secretDescriptors,
DaprClient client,
- TimeSpan sidecarWaitTimeout)
+ TimeSpan sidecarWaitTimeout,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
@@ -78,7 +83,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
Store = store,
SecretDescriptors = secretDescriptors,
Client = client,
- SidecarWaitTimeout = sidecarWaitTimeout
+ SidecarWaitTimeout = sidecarWaitTimeout,
+ IsOptional = optional
});
return configurationBuilder;
@@ -89,14 +95,16 @@ public static IConfigurationBuilder AddDaprSecretStore(
///
/// The to add to.
/// Dapr secret store name.
- /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.
/// The Dapr client
+ /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.
+ /// When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddDaprSecretStore(
this IConfigurationBuilder configurationBuilder,
string store,
DaprClient client,
- IReadOnlyDictionary? metadata = null)
+ IReadOnlyDictionary? metadata = null,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -105,7 +113,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
{
Store = store,
Metadata = metadata,
- Client = client
+ Client = client,
+ IsOptional = optional
});
return configurationBuilder;
@@ -116,16 +125,18 @@ public static IConfigurationBuilder AddDaprSecretStore(
///
/// The to add to.
/// Dapr secret store name.
- /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.
/// The Dapr client
/// The used to configure the timeout waiting for Dapr.
+ /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.
+ /// When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddDaprSecretStore(
this IConfigurationBuilder configurationBuilder,
string store,
DaprClient client,
TimeSpan sidecarWaitTimeout,
- IReadOnlyDictionary? metadata = null)
+ IReadOnlyDictionary? metadata = null,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -135,7 +146,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
Store = store,
Metadata = metadata,
Client = client,
- SidecarWaitTimeout = sidecarWaitTimeout
+ SidecarWaitTimeout = sidecarWaitTimeout,
+ IsOptional = optional
});
return configurationBuilder;
@@ -146,14 +158,16 @@ public static IConfigurationBuilder AddDaprSecretStore(
///
/// The to add to.
/// Dapr secret store name.
- /// A collection of delimiters that will be replaced by ':' in the key of every secret.
/// The Dapr client
+ /// A collection of delimiters that will be replaced by ':' in the key of every secret.
+ /// When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddDaprSecretStore(
this IConfigurationBuilder configurationBuilder,
string store,
DaprClient client,
- IEnumerable? keyDelimiters)
+ IEnumerable? keyDelimiters,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -161,7 +175,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
var source = new DaprSecretStoreConfigurationSource
{
Store = store,
- Client = client
+ Client = client,
+ IsOptional = optional
};
if (keyDelimiters != null)
@@ -179,16 +194,18 @@ public static IConfigurationBuilder AddDaprSecretStore(
///
/// The to add to.
/// Dapr secret store name.
- /// A collection of delimiters that will be replaced by ':' in the key of every secret.
/// The Dapr client
+ /// A collection of delimiters that will be replaced by ':' in the key of every secret.
/// The used to configure the timeout waiting for Dapr.
+ /// When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.
/// The .
public static IConfigurationBuilder AddDaprSecretStore(
this IConfigurationBuilder configurationBuilder,
string store,
DaprClient client,
IEnumerable? keyDelimiters,
- TimeSpan sidecarWaitTimeout)
+ TimeSpan sidecarWaitTimeout,
+ bool optional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -197,7 +214,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
{
Store = store,
Client = client,
- SidecarWaitTimeout = sidecarWaitTimeout
+ SidecarWaitTimeout = sidecarWaitTimeout,
+ IsOptional = optional
};
if (keyDelimiters != null)
@@ -218,4 +236,4 @@ public static IConfigurationBuilder AddDaprSecretStore(
/// The .
public static IConfigurationBuilder AddDaprSecretStore(this IConfigurationBuilder configurationBuilder, Action configureSource)
=> configurationBuilder.Add(configureSource);
-}
\ No newline at end of file
+}
diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs
index 31aa5b1fc..85ab2c489 100644
--- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs
+++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs
@@ -24,7 +24,7 @@ namespace Dapr.Extensions.Configuration.DaprSecretStore;
///
/// A Dapr Secret Store based .
///
-internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider
+internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider, IDisposable
{
internal static readonly TimeSpan DefaultSidecarWaitTimeout = TimeSpan.FromSeconds(5);
@@ -42,6 +42,10 @@ internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider
private readonly TimeSpan sidecarWaitTimeout;
+ private readonly bool isOptional;
+
+ private readonly CancellationTokenSource cts = new();
+
///
/// Creates a new instance of .
///
@@ -83,13 +87,15 @@ public DaprSecretStoreConfigurationProvider(
/// The secrets to retrieve.
/// Dapr client used to retrieve Secrets
/// The used to configure the timeout waiting for Dapr.
+ /// When true, does not block startup waiting for the sidecar.
public DaprSecretStoreConfigurationProvider(
string store,
bool normalizeKey,
IList? keyDelimiters,
IEnumerable secretDescriptors,
DaprClient client,
- TimeSpan sidecarWaitTimeout)
+ TimeSpan sidecarWaitTimeout,
+ bool isOptional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
@@ -106,6 +112,7 @@ public DaprSecretStoreConfigurationProvider(
this.secretDescriptors = secretDescriptors;
this.client = client;
this.sidecarWaitTimeout = sidecarWaitTimeout;
+ this.isOptional = isOptional;
}
///
@@ -149,13 +156,15 @@ public DaprSecretStoreConfigurationProvider(
/// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.
/// Dapr client used to retrieve Secrets
/// The used to configure the timeout waiting for Dapr.
+ /// When true, does not block startup waiting for the sidecar.
public DaprSecretStoreConfigurationProvider(
string store,
bool normalizeKey,
IList? keyDelimiters,
IReadOnlyDictionary? metadata,
DaprClient client,
- TimeSpan sidecarWaitTimeout)
+ TimeSpan sidecarWaitTimeout,
+ bool isOptional = false)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -166,6 +175,14 @@ public DaprSecretStoreConfigurationProvider(
this.metadata = metadata;
this.client = client;
this.sidecarWaitTimeout = sidecarWaitTimeout;
+ this.isOptional = isOptional;
+ }
+
+ ///
+ public void Dispose()
+ {
+ cts.Cancel();
+ cts.Dispose();
}
private string NormalizeKey(string key)
@@ -185,18 +202,72 @@ private string NormalizeKey(string key)
/// Loads the configuration by calling the asynchronous LoadAsync method and blocking the calling
/// thread until the operation is completed.
///
- public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+ public override void Load()
+ {
+ if (isOptional)
+ {
+ Data = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _ = Task.Run(() => LoadInBackgroundAsync());
+ }
+ else
+ {
+ LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+ }
- private async Task LoadAsync()
+ private async Task LoadInBackgroundAsync()
{
- var data = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+ while (!cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ using var tokenSource = new CancellationTokenSource(sidecarWaitTimeout);
+ using var linked = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token, cts.Token);
+ await client.WaitForSidecarAsync(linked.Token);
+
+ await FetchSecretsAsync();
+ OnReload();
+ return;
+ }
+ catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
+ {
+ return;
+ }
+ catch (OperationCanceledException)
+ {
+ // Sidecar wait timed out — retry after delay.
+ }
+ catch (DaprException)
+ {
+ // Transient Dapr error — retry after delay.
+ }
+
+ try
+ {
+ await Task.Delay(sidecarWaitTimeout, cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+ }
+ private async Task LoadAsync()
+ {
// Wait for the Dapr Sidecar to report healthy before attempting to fetch secrets.
using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout))
{
await client.WaitForSidecarAsync(tokenSource.Token);
}
+ await FetchSecretsAsync();
+ }
+
+ private async Task FetchSecretsAsync()
+ {
+ var data = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
if (secretDescriptors != null)
{
foreach (var secretDescriptor in secretDescriptors)
diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs
index de73c8518..d21dd4158 100644
--- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs
+++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs
@@ -59,6 +59,13 @@ public class DaprSecretStoreConfigurationSource : IConfigurationSource
///
public TimeSpan? SidecarWaitTimeout { get; set; }
+ ///
+ /// Gets or sets a value indicating whether this configuration source is optional.
+ /// When true, the provider will not block startup waiting for the Dapr sidecar and will
+ /// instead load secrets in the background once the sidecar becomes available.
+ ///
+ public bool IsOptional { get; set; }
+
///
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
@@ -69,11 +76,11 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
throw new ArgumentException($"{nameof(Metadata)} must be null when {nameof(SecretDescriptors)} is set", nameof(Metadata));
}
- return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, SecretDescriptors, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout);
+ return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, SecretDescriptors, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout, IsOptional);
}
else
{
- return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, Metadata, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout);
+ return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, Metadata, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout, IsOptional);
}
}
}
\ No newline at end of file
diff --git a/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs
index a1cb299f7..db4441fb9 100644
--- a/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs
+++ b/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs
@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Net;
+using System.Threading;
using System.Threading.Tasks;
using Dapr.Client;
using Grpc.Net.Client;
using Microsoft.Extensions.Configuration;
+using Moq;
+using Shouldly;
using Xunit;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
@@ -187,6 +190,162 @@ private async Task SendResponseWithConfiguration(Dictionary();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(ct =>
+ Task.Delay(Timeout.Infinite, ct));
+
+ var config = new ConfigurationBuilder()
+ .AddDaprConfigurationStore("store", new List(), daprClient.Object, TimeSpan.FromSeconds(1), optional: true)
+ .Build();
+
+ // Build() should succeed immediately with no values
+ config["anyKey"].ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task TestConfigurationStore_OptionalAndSidecarBecomesAvailable_PopulatesConfig()
+ {
+ var callCount = 0;
+
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(ct =>
+ {
+ var current = Interlocked.Increment(ref callCount);
+ if (current <= 1)
+ {
+ return Task.Delay(Timeout.Infinite, ct);
+ }
+ return Task.CompletedTask;
+ });
+
+ var configResponse = new GetConfigurationResponse(
+ new Dictionary
+ {
+ ["testKey"] = new ConfigurationItem("testValue", "v1", null)
+ });
+
+ daprClient
+ .Setup(c => c.GetConfiguration(
+ "store",
+ It.IsAny>(),
+ It.IsAny>(),
+ It.IsAny()))
+ .ReturnsAsync(configResponse);
+
+ var config = new ConfigurationBuilder()
+ .AddDaprConfigurationStore("store", new List(), daprClient.Object, TimeSpan.FromMilliseconds(100), optional: true)
+ .Build();
+
+ // Initially empty
+ config["testKey"].ShouldBeNull();
+
+ // Wait for the reload token to fire, indicating background load completed
+ var reloaded = new TaskCompletionSource();
+ config.GetReloadToken().RegisterChangeCallback(_ => reloaded.TrySetResult(true), null);
+ await reloaded.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ config["testKey"].ShouldBe("testValue");
+ }
+
+ [Fact]
+ public async Task TestConfigurationStore_OptionalAndFetchThrowsTransientError_RetriesAndSucceeds()
+ {
+ var callCount = 0;
+
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ var configResponse = new GetConfigurationResponse(
+ new Dictionary
+ {
+ ["testKey"] = new ConfigurationItem("testValue", "v1", null)
+ });
+
+ daprClient
+ .Setup(c => c.GetConfiguration(
+ "store",
+ It.IsAny>(),
+ It.IsAny>(),
+ It.IsAny()))
+ .Returns(() =>
+ {
+ var current = Interlocked.Increment(ref callCount);
+ if (current <= 1)
+ {
+ throw new DaprException("transient error");
+ }
+ return Task.FromResult(configResponse);
+ });
+
+ var config = new ConfigurationBuilder()
+ .AddDaprConfigurationStore("store", new List(), daprClient.Object, TimeSpan.FromMilliseconds(100), optional: true)
+ .Build();
+
+ // Initially empty
+ config["testKey"].ShouldBeNull();
+
+ // Wait for the reload token to fire after retry succeeds
+ var reloaded = new TaskCompletionSource();
+ config.GetReloadToken().RegisterChangeCallback(_ => reloaded.TrySetResult(true), null);
+ await reloaded.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ config["testKey"].ShouldBe("testValue");
+ }
+
+ [Fact]
+ public async Task TestConfigurationStore_OptionalAndDisposed_ExitsCleanly()
+ {
+ var waitCalled = new TaskCompletionSource();
+
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(ct =>
+ {
+ waitCalled.TrySetResult(true);
+ return Task.Delay(Timeout.Infinite, ct);
+ });
+
+ var config = new ConfigurationBuilder()
+ .AddDaprConfigurationStore("store", new List(), daprClient.Object, TimeSpan.FromSeconds(1), optional: true)
+ .Build();
+
+ // Ensure background task has started
+ await waitCalled.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ // Dispose the provider to cancel the background task
+ (config as IDisposable)?.Dispose();
+
+ // Verify the mock was called with a token that is now cancelled
+ daprClient.Verify(c => c.WaitForSidecarAsync(It.IsAny()), Times.AtLeastOnce());
+ config["anyKey"].ShouldBeNull();
+ }
+
+ [Fact]
+ public void TestStreamingConfigurationStore_OptionalAndSidecarUnavailable_ReturnsEmptyConfig()
+ {
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(ct =>
+ Task.Delay(Timeout.Infinite, ct));
+
+ var config = new ConfigurationBuilder()
+ .AddStreamingDaprConfigurationStore("store", new List(), daprClient.Object, TimeSpan.FromSeconds(1), optional: true)
+ .Build();
+
+ config["anyKey"].ShouldBeNull();
+ }
+
private async Task SendStreamingResponseWithConfiguration(Dictionary items, TestHttpClient.Entry entry)
{
var streamResponse = new Autogenerated.SubscribeConfigurationResponse();
diff --git a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs
index 53cb4fc6c..c6453ef08 100644
--- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs
+++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs
@@ -14,6 +14,7 @@
using System;
using System.Collections.Generic;
using System.Net;
+using System.Threading;
using System.Threading.Tasks;
using Dapr.Client;
using Grpc.Net.Client;
@@ -897,6 +898,166 @@ public void LoadSecrets_FailsIfSidecarNotAvailable()
.Build());
}
+ [Fact]
+ public void LoadSecrets_OptionalAndSidecarUnavailable_ReturnsEmptyConfig()
+ {
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(ct =>
+ Task.Delay(Timeout.Infinite, ct));
+
+ var config = CreateBuilder()
+ .AddDaprSecretStore("store", daprClient.Object, optional: true)
+ .Build();
+
+ // Build() should succeed immediately with no values
+ config["anyKey"].ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task LoadSecrets_OptionalAndSidecarBecomesAvailable_PopulatesConfig()
+ {
+ var callCount = 0;
+
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(ct =>
+ {
+ var current = Interlocked.Increment(ref callCount);
+ if (current <= 1)
+ {
+ // First call: simulate sidecar not ready by waiting until timeout cancels
+ return Task.Delay(Timeout.Infinite, ct);
+ }
+ // Subsequent calls: sidecar is ready
+ return Task.CompletedTask;
+ });
+
+ daprClient
+ .Setup(c => c.GetBulkSecretAsync("store", null, default))
+ .ReturnsAsync(new Dictionary>
+ {
+ ["secret1"] = new Dictionary { ["secret1"] = "value1" }
+ });
+
+ var config = CreateBuilder()
+ .AddDaprSecretStore("store", daprClient.Object, TimeSpan.FromMilliseconds(100), optional: true)
+ .Build();
+
+ // Initially empty
+ config["secret1"].ShouldBeNull();
+
+ // Wait for the reload token to fire, indicating background load completed
+ var reloaded = new TaskCompletionSource();
+ config.GetReloadToken().RegisterChangeCallback(_ => reloaded.TrySetResult(true), null);
+ await reloaded.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ config["secret1"].ShouldBe("value1");
+ }
+
+ [Fact]
+ public async Task LoadSecrets_OptionalWithDescriptors_PopulatesConfig()
+ {
+ var storeName = "store";
+ var secretKey = "mySecret";
+ var secretValue = "myValue";
+
+ var daprClient = new Mock();
+ // Sidecar ready immediately
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ daprClient
+ .Setup(c => c.GetSecretAsync(storeName, secretKey, It.IsAny>(), default))
+ .ReturnsAsync(new Dictionary { { secretKey, secretValue } });
+
+ var secretDescriptors = new[] { new DaprSecretDescriptor(secretKey) };
+
+ var config = CreateBuilder()
+ .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object, TimeSpan.FromSeconds(1), optional: true)
+ .Build();
+
+ // Wait for the reload token to fire, indicating background load completed
+ var reloaded = new TaskCompletionSource();
+ config.GetReloadToken().RegisterChangeCallback(_ => reloaded.TrySetResult(true), null);
+ await reloaded.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ config[secretKey].ShouldBe(secretValue);
+ }
+
+ [Fact]
+ public async Task LoadSecrets_OptionalAndFetchThrowsTransientError_RetriesAndSucceeds()
+ {
+ var callCount = 0;
+
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ daprClient
+ .Setup(c => c.GetBulkSecretAsync("store", null, default))
+ .Returns(() =>
+ {
+ var current = Interlocked.Increment(ref callCount);
+ if (current <= 1)
+ {
+ throw new DaprException("transient error");
+ }
+ return Task.FromResult>>(
+ new Dictionary>
+ {
+ ["secret1"] = new Dictionary { ["secret1"] = "value1" }
+ });
+ });
+
+ var config = CreateBuilder()
+ .AddDaprSecretStore("store", daprClient.Object, TimeSpan.FromMilliseconds(100), optional: true)
+ .Build();
+
+ // Initially empty
+ config["secret1"].ShouldBeNull();
+
+ // Wait for the reload token to fire after retry succeeds
+ var reloaded = new TaskCompletionSource();
+ config.GetReloadToken().RegisterChangeCallback(_ => reloaded.TrySetResult(true), null);
+ await reloaded.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ config["secret1"].ShouldBe("value1");
+ }
+
+ [Fact]
+ public async Task LoadSecrets_OptionalAndDisposed_ExitsCleanly()
+ {
+ var waitCalled = new TaskCompletionSource();
+
+ var daprClient = new Mock();
+ daprClient
+ .Setup(c => c.WaitForSidecarAsync(It.IsAny()))
+ .Returns(ct =>
+ {
+ waitCalled.TrySetResult(true);
+ return Task.Delay(Timeout.Infinite, ct);
+ });
+
+ var config = CreateBuilder()
+ .AddDaprSecretStore("store", daprClient.Object, TimeSpan.FromSeconds(1), optional: true)
+ .Build();
+
+ // Ensure background task has started
+ await waitCalled.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ // Dispose the provider to cancel the background task
+ (config as IDisposable)?.Dispose();
+
+ // Verify the mock was called with a token that is now cancelled
+ daprClient.Verify(c => c.WaitForSidecarAsync(It.IsAny()), Times.AtLeastOnce());
+ config["anyKey"].ShouldBeNull();
+ }
+
private IConfigurationBuilder CreateBuilder()
{
return new ConfigurationBuilder();