diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml
index 4dcdfb951..0c96fa9ca 100644
--- a/.github/workflows/itests.yml
+++ b/.github/workflows/itests.yml
@@ -48,8 +48,8 @@ jobs:
GOOS: linux
GOARCH: amd64
GOPROXY: https://proxy.golang.org
- DAPR_CLI_VER: 1.14.0
- DAPR_RUNTIME_VER: 1.14.0
+ DAPR_CLI_VER: 1.15.0
+ DAPR_RUNTIME_VER: 1.15.3
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh
DAPR_CLI_REF: ''
steps:
diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md
index 09422e474..a5d60c2fa 100644
--- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md
+++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md
@@ -20,7 +20,7 @@ To load secrets into configuration call the _AddDaprSecretStore_ extension metho
Use Dapr to run the application:
```shell
-dapr run --app-id SecretStoreConfigurationProviderSample --components-path ./components/ -- dotnet run
+dapr run --app-id SecretStoreConfigurationProviderSample --resources-path ./components/ -- dotnet run
```
### 2. Test the application
diff --git a/examples/Client/ConfigurationApi/README.md b/examples/Client/ConfigurationApi/README.md
index 7425a780a..d73a29f9f 100644
--- a/examples/Client/ConfigurationApi/README.md
+++ b/examples/Client/ConfigurationApi/README.md
@@ -147,7 +147,7 @@ cd examples/Client/ConfigurationApi
To run the `ConfigurationExample`, execute the following command:
```bash
-dapr run --app-id configexample --components-path ./Components -- dotnet run
+dapr run --app-id configexample --resources-path ./Components -- dotnet run
```
### Get Configuration
diff --git a/examples/Client/DistributedLock/README.md b/examples/Client/DistributedLock/README.md
index cdac6f91a..6a1af3b34 100644
--- a/examples/Client/DistributedLock/README.md
+++ b/examples/Client/DistributedLock/README.md
@@ -24,7 +24,7 @@ cd examples/Client/DistributedLock
In order to run the application that generates data for the workers to process, simply run the following command:
```bash
-dapr run --components-path ./Components --app-id generator -- dotnet run
+dapr run --resources-path ./Components --app-id generator -- dotnet run
```
This application will create a new file to process once every 10 seconds. The files are stored in `DistributedLock/tmp`.
@@ -33,8 +33,8 @@ This application will create a new file to process once every 10 seconds. The fi
In order to properly demonstrate locking, this application will be run more than once with the same App ID. However, the applications do need different ports in order to properly receive bindings. Run them with the command below:
```bash
-dapr run --components-path ./Components --app-id worker --app-port 5000 -- dotnet run
-dapr run --components-path ./Components --app-id worker --app-port 5001 -- dotnet run
+dapr run --resources-path ./Components --app-id worker --app-port 5000 -- dotnet run
+dapr run --resources-path ./Components --app-id worker --app-port 5001 -- dotnet run
```
After running the applications, they will attempt to process files. You should see output such as:
diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs
index e0d91c44f..dde86b918 100644
--- a/src/Dapr.Actors/IDaprInteractor.cs
+++ b/src/Dapr.Actors/IDaprInteractor.cs
@@ -11,110 +11,108 @@
// limitations under the License.
// ------------------------------------------------------------------------
+namespace Dapr.Actors;
+
using System.Net.Http;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.Actors.Communication;
-namespace Dapr.Actors
+///
+/// Interface for interacting with Dapr runtime.
+///
+internal interface IDaprInteractor
{
- using System.IO;
- using System.Threading;
- using System.Threading.Tasks;
- using Dapr.Actors.Communication;
-
///
- /// Interface for interacting with Dapr runtime.
+ /// Invokes an Actor method on Dapr without remoting.
///
- internal interface IDaprInteractor
- {
- ///
- /// Invokes an Actor method on Dapr without remoting.
- ///
- /// Type of actor.
- /// ActorId.
- /// Method name to invoke.
- /// Serialized body.
- /// Cancels the operation.
- /// A task that represents the asynchronous operation.
- Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default);
+ /// Type of actor.
+ /// ActorId.
+ /// Method name to invoke.
+ /// Serialized body.
+ /// Cancels the operation.
+ /// A task that represents the asynchronous operation.
+ Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default);
- ///
- /// Saves state batch to Dapr.
- ///
- /// Type of actor.
- /// ActorId.
- /// JSON data with state changes as per the Dapr spec for transaction state update.
- /// Cancels the operation.
- /// A task that represents the asynchronous operation.
- Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default);
+ ///
+ /// Saves state batch to Dapr.
+ ///
+ /// Type of actor.
+ /// ActorId.
+ /// JSON data with state changes as per the Dapr spec for transaction state update.
+ /// Cancels the operation.
+ /// A task that represents the asynchronous operation.
+ Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default);
- ///
- /// Saves a state to Dapr.
- ///
- /// Type of actor.
- /// ActorId.
- /// Name of key to get value for.
- /// Cancels the operation.
- /// A task that represents the asynchronous operation.
- Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default);
+ ///
+ /// Gets a state from Dapr.
+ ///
+ /// Type of actor.
+ /// ActorId.
+ /// Name of key to get value for.
+ /// Cancels the operation.
+ /// A task that represents the asynchronous operation.
+ Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default);
- ///
- /// Invokes Actor method.
- ///
- /// Serializers manager for remoting calls.
- /// Actor Request Message.
- /// Cancels the operation.
- /// A representing the result of the asynchronous operation.
- Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default);
+ ///
+ /// Invokes Actor method.
+ ///
+ /// Serializers manager for remoting calls.
+ /// Actor Request Message.
+ /// Cancels the operation.
+ /// A representing the result of the asynchronous operation.
+ Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default);
- ///
- /// Register a reminder.
- ///
- /// Type of actor.
- /// ActorId.
- /// Name of reminder to register.
- /// JSON reminder data as per the Dapr spec.
- /// Cancels the operation.
- /// A representing the result of the asynchronous operation.
- Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default);
+ ///
+ /// Register a reminder.
+ ///
+ /// Type of actor.
+ /// ActorId.
+ /// Name of reminder to register.
+ /// JSON reminder data as per the Dapr spec.
+ /// Cancels the operation.
+ /// A representing the result of the asynchronous operation.
+ Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default);
- ///
- /// Gets a reminder.
- ///
- /// Type of actor.
- /// ActorId.
- /// Name of reminder to unregister.
- /// Cancels the operation.
- /// A containing the response of the asynchronous HTTP operation.
- Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default);
+ ///
+ /// Gets a reminder.
+ ///
+ /// Type of actor.
+ /// ActorId.
+ /// Name of reminder to unregister.
+ /// Cancels the operation.
+ /// A containing the response of the asynchronous HTTP operation.
+ Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default);
- ///
- /// Unregisters a reminder.
- ///
- /// Type of actor.
- /// ActorId.
- /// Name of reminder to unregister.
- /// Cancels the operation.
- /// A representing the result of the asynchronous operation.
- Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default);
+ ///
+ /// Unregisters a reminder.
+ ///
+ /// Type of actor.
+ /// ActorId.
+ /// Name of reminder to unregister.
+ /// Cancels the operation.
+ /// A representing the result of the asynchronous operation.
+ Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default);
- ///
- /// Registers a timer.
- ///
- /// Type of actor.
- /// ActorId.
- /// Name of timer to register.
- /// JSON reminder data as per the Dapr spec.
- /// Cancels the operation.
- /// A representing the result of the asynchronous operation.
- Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default);
+ ///
+ /// Registers a timer.
+ ///
+ /// Type of actor.
+ /// ActorId.
+ /// Name of timer to register.
+ /// JSON reminder data as per the Dapr spec.
+ /// Cancels the operation.
+ /// A representing the result of the asynchronous operation.
+ Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default);
- ///
- /// Unegisters a timer.
- ///
- /// Type of actor.
- /// ActorId.
- /// Name of timer to register.
- /// Cancels the operation.
- /// A representing the result of the asynchronous operation.
- Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default);
- }
+ ///
+ /// Unegisters a timer.
+ ///
+ /// Type of actor.
+ /// ActorId.
+ /// Name of timer to register.
+ /// Cancels the operation.
+ /// A representing the result of the asynchronous operation.
+ Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default);
}
diff --git a/src/Dapr.Actors/Runtime/ActorStateCache.cs b/src/Dapr.Actors/Runtime/ActorStateCache.cs
new file mode 100644
index 000000000..666cb6625
--- /dev/null
+++ b/src/Dapr.Actors/Runtime/ActorStateCache.cs
@@ -0,0 +1,224 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+#nullable enable
+using System;
+using System.Collections.Generic;
+
+namespace Dapr.Actors.Runtime;
+
+internal sealed class ActorStateCache : IActorStateCache
+{
+ ///
+ /// Maintains the cache state.
+ ///
+ private readonly Dictionary stateMetadata = new();
+
+ ///
+ /// Adds the indicated value to the cache.
+ ///
+ /// The name of the state.
+ /// The cached value.
+ /// How far out the TTL expiry should be.
+ /// The type of value getting cached.
+ /// stateContainsKey indicates if the cache already contains the key or not and
+ /// addedToState indicates if the value was added or updated in the cache.
+ public (bool stateContainsKey, bool addedToState) Add(string stateName, T value, TimeSpan? ttl = null)
+ {
+ if (!stateMetadata.TryGetValue(stateName, out var state))
+ {
+ stateMetadata.Add(stateName, StateMetadata.Create(value, StateChangeKind.Add, ttl));
+ return (false, true);
+ }
+
+ if (!IsMarkedAsRemoveOrExpired(state))
+ {
+ return (true, false);
+ }
+
+ stateMetadata[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl);
+ return (true, true);
+ }
+
+ ///
+ /// Adds the indicated value to the cache.
+ ///
+ /// The name of the state.
+ /// The cached value.
+ /// The TTL expiry timestamp.
+ /// The type of value getting cached.
+ /// stateContainsKey indicates if the cache already contains the key or not and
+ /// addedToState indicates if the value was added or updated in the cache.
+ public (bool stateContainsKey, bool addedToState) Add(string stateName, T value, DateTimeOffset ttlExpiry)
+ {
+ if (!stateMetadata.TryGetValue(stateName, out var state))
+ {
+ stateMetadata.Add(stateName, StateMetadata.Create(value, StateChangeKind.Add, ttlExpiry));
+ return (false, true);
+ }
+
+ if (!IsMarkedAsRemoveOrExpired(state))
+ {
+ return (true, false);
+ }
+
+ stateMetadata[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttlExpiry);
+ return (true, true);
+
+ }
+
+ ///
+ /// Sets the cache with the specified value whether it already exists or not.
+ ///
+ /// The name of the state to save the value to.
+ /// The state metadata to save to the cache.
+ public void Set(string stateName, StateMetadata metadata)
+ {
+ stateMetadata[stateName] = metadata;
+ }
+
+ ///
+ /// Removes the indicated state name from the cache.
+ ///
+ /// The name of the state to remove.
+ public void Remove(string stateName) => stateMetadata.Remove(stateName);
+
+ ///
+ /// Retrieves the current state from the cache if available and not expired.
+ ///
+ /// The name of the state to retrieve.
+ /// If available and not expired, the value of the state persisted in the cache.
+ /// True if the cache contains the state name; false if not.
+ public (bool containsKey, bool isMarkedAsRemoveOrExpired) TryGet(string stateName, out StateMetadata? metadata)
+ {
+ var isMarkedAsRemoveOrExpired = false;
+ metadata = null;
+
+ if (!stateMetadata.TryGetValue(stateName, out var state))
+ {
+ return (false, false);
+ }
+
+ if (IsMarkedAsRemoveOrExpired(state))
+ {
+ isMarkedAsRemoveOrExpired = true;
+ }
+
+ metadata = state;
+ return (true, isMarkedAsRemoveOrExpired);
+
+ }
+
+ ///
+ /// Clears the all the data from the cache.
+ ///
+ public void Clear()
+ {
+ stateMetadata.Clear();
+ }
+
+ ///
+ /// Builds out the change lists of states to update in the provider and states to remove from the cache. This
+ /// is typically only called by invocation of the SaveStateAsync method in .
+ ///
+ /// The list of state changes and states to remove from the cache.
+ public (IReadOnlyList stateChanges, IReadOnlyList statesToRemove) BuildChangeList()
+ {
+ var stateChanges = new List();
+ var statesToRemove = new List();
+
+ if (stateMetadata.Count == 0)
+ {
+ return (stateChanges, statesToRemove);
+ }
+
+ foreach (var stateName in stateMetadata.Keys)
+ {
+ var metadata = stateMetadata[stateName];
+ if (metadata.ChangeKind is not StateChangeKind.None)
+ {
+ stateChanges.Add(new ActorStateChange(stateName, metadata.Type, metadata.Value, metadata.ChangeKind, metadata.TTLExpireTime));
+
+ if (metadata.ChangeKind is StateChangeKind.Remove)
+ {
+ statesToRemove.Add(stateName);
+ }
+
+ //Mark the states as unmodified so the tracking for the next invocation is done correctly
+ var updatedState = metadata with { ChangeKind = StateChangeKind.None };
+ stateMetadata[stateName] = updatedState;
+ }
+ }
+
+ return (stateChanges, statesToRemove);
+ }
+
+ ///
+ /// Helper method that determines if a state metadata is expired.
+ ///
+ /// The metadata to evaluate.
+ /// True if the state metadata is marked for removal or the TTL has expired, otherwise false.
+ public bool IsMarkedAsRemoveOrExpired(StateMetadata metadata) =>
+ metadata.ChangeKind == StateChangeKind.Remove || (metadata.TTLExpireTime.HasValue &&
+ metadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow);
+
+ ///
+ /// Exposed for testing only.
+ ///
+ ///
+ internal Dictionary GetStateMetadata() => stateMetadata;
+
+ internal sealed record StateMetadata
+ {
+ ///
+ /// This should only be used for testing purposes. Use the static `Create` methods for any actual usage.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal StateMetadata(object? value, Type type, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime = null, TimeSpan? ttl = null)
+ {
+ this.Value = value;
+ this.Type = type;
+ this.ChangeKind = changeKind;
+
+ if (ttlExpireTime.HasValue && ttl.HasValue) {
+ throw new ArgumentException("Cannot specify both TTLExpireTime and TTL");
+ }
+
+ this.TTLExpireTime = ttl.HasValue ? DateTimeOffset.UtcNow.Add(ttl.Value) : ttlExpireTime;
+ }
+
+ public object? Value { get; init; }
+
+ public StateChangeKind ChangeKind { get; init; }
+
+ public Type Type { get; init; }
+
+ public DateTimeOffset? TTLExpireTime { get; init; }
+
+ public static StateMetadata Create(T? value, StateChangeKind changeKind) =>
+ new(value, typeof(T), changeKind);
+
+ public static StateMetadata Create(T? value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) =>
+ new(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime);
+
+ public static StateMetadata Create(T? value, StateChangeKind changeKind, TimeSpan? ttl) =>
+ new(value, typeof(T), changeKind, ttl: ttl);
+
+ public static StateMetadata CreateForRemove() => new(null, typeof(object), StateChangeKind.Remove);
+ }
+}
diff --git a/src/Dapr.Actors/Runtime/ActorStateChange.cs b/src/Dapr.Actors/Runtime/ActorStateChange.cs
index 34fa68fdf..f90338f6d 100644
--- a/src/Dapr.Actors/Runtime/ActorStateChange.cs
+++ b/src/Dapr.Actors/Runtime/ActorStateChange.cs
@@ -1,5 +1,5 @@
// ------------------------------------------------------------------------
-// Copyright 2021 The Dapr Authors
+// Copyright 2025 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -11,75 +11,22 @@
// limitations under the License.
// ------------------------------------------------------------------------
-namespace Dapr.Actors.Runtime
-{
- using System;
-
- ///
- /// Represents a change to an actor state with a given state name.
- ///
- public sealed class ActorStateChange
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The name of the actor state.
- /// The type of value associated with given actor state name.
- /// The value associated with given actor state name.
- /// The kind of state change for given actor state name.
- /// The time to live for the state.
- public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime)
- {
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
-
- this.StateName = stateName;
- this.Type = type;
- this.Value = value;
- this.ChangeKind = changeKind;
- this.TTLExpireTime = ttlExpireTime;
- }
-
- ///
- /// Gets the name of the actor state.
- ///
- ///
- /// The name of the actor state.
- ///
- public string StateName { get; }
-
- ///
- /// Gets the type of value associated with given actor state name.
- ///
- ///
- /// The type of value associated with given actor state name.
- ///
- public Type Type { get; }
-
- ///
- /// Gets the value associated with given actor state name.
- ///
- ///
- /// The value associated with given actor state name.
- ///
- public object Value { get; }
-
- ///
- /// Gets the kind of state change for given actor state name.
- ///
- ///
- /// The kind of state change for given actor state name.
- ///
- public StateChangeKind ChangeKind { get; }
-
- ///
- /// Gets the time to live for the state.
- ///
- ///
- /// The time to live for the state.
- ///
- ///
- /// If null, the state will not expire.
- ///
- public DateTimeOffset? TTLExpireTime { get; }
- }
-}
+#nullable enable
+namespace Dapr.Actors.Runtime;
+
+using System;
+
+///
+/// Represents a change to an actor state with a given state name.
+///
+/// The name of the actor state.
+/// The type of value associated with the given actor state name.
+/// The value associated with the given actor state name.
+/// The kind of state change for the given actor state name.
+/// The time to live for the state. If null, the state wil not expire.
+public sealed record ActorStateChange(
+ string StateName,
+ Type Type,
+ object? Value,
+ StateChangeKind ChangeKind,
+ DateTimeOffset? TTLExpireTime);
diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs
index 31ada4433..b496f1589 100644
--- a/src/Dapr.Actors/Runtime/ActorStateManager.cs
+++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs
@@ -19,567 +19,458 @@
using Dapr.Actors.Resources;
using Dapr.Actors.Communication;
-namespace Dapr.Actors.Runtime
+namespace Dapr.Actors.Runtime;
+
+internal sealed class ActorStateManager : IActorStateManager, IActorContextualState
{
- internal sealed class ActorStateManager : IActorStateManager, IActorContextualState
+ private readonly Actor actor;
+ private readonly string actorTypeName;
+ private readonly IActorStateCache defaultCache;
+ private static readonly AsyncLocal<(string id, IActorStateCache stateCache)> context = new();
+
+ internal ActorStateManager(Actor actor)
{
- private readonly Actor actor;
- private readonly string actorTypeName;
- private readonly Dictionary defaultTracker;
- private static AsyncLocal<(string id, Dictionary tracker)> context = new AsyncLocal<(string, Dictionary)>();
+ this.actor = actor;
+ this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName;
+ this.defaultCache = new ActorStateCache();
+ }
- internal ActorStateManager(Actor actor)
- {
- this.actor = actor;
- this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName;
- this.defaultTracker = new Dictionary();
- }
+ internal ActorStateManager(Actor actor, IActorStateCache stateCache)
+ {
+ this.actor = actor;
+ this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName;
+ this.defaultCache = stateCache;
+ }
- public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken)
- {
- EnsureStateProviderInitialized();
+ public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
- if (!(await this.TryAddStateAsync(stateName, value, cancellationToken)))
- {
- throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName));
- }
+ if (!(await this.TryAddStateAsync(stateName, value, cancellationToken)))
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName));
}
+ }
- public async Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken)
- {
- EnsureStateProviderInitialized();
+ public async Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
- if (!(await this.TryAddStateAsync(stateName, value, ttl, cancellationToken)))
- {
- throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName));
- }
+ if (!(await this.TryAddStateAsync(stateName, value, ttl, cancellationToken)))
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName));
}
+ }
- public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default)
- {
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
+ public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- EnsureStateProviderInitialized();
+ EnsureStateProviderInitialized();
- var stateChangeTracker = GetContextualStateTracker();
+ var cache = GetContextualStateTracker();
+ var (stateContainsKey, addedToState) = cache.Add(stateName, value);
+ if (stateContainsKey)
+ {
+ return addedToState;
+ }
- if (stateChangeTracker.ContainsKey(stateName))
- {
- var stateMetadata = stateChangeTracker[stateName];
+ var containsStateResult = await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName,
+ this.actor.Id.ToString(), stateName, cancellationToken);
+ if (containsStateResult)
+ {
+ //Return false because we shouldn't add a value already present in the provider
+ return false;
+ }
- // Check if the property was marked as remove or is expired in the cache
- if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow))
- {
- stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update);
- return true;
- }
+ //Add to the cache
+ cache.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add));
+ return addedToState;
+ }
- return false;
- }
+ public async Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
- {
- return false;
- }
+ EnsureStateProviderInitialized();
- stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add);
- return true;
+ var cache = GetContextualStateTracker();
+ var (stateContainsKey, addedToState) = cache.Add(stateName, value, ttl);
+ if (stateContainsKey)
+ {
+ return addedToState;
}
-
- public async Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default)
+
+ if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
{
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
-
- EnsureStateProviderInitialized();
-
- var stateChangeTracker = GetContextualStateTracker();
-
- if (stateChangeTracker.ContainsKey(stateName))
- {
- var stateMetadata = stateChangeTracker[stateName];
+ return false;
+ }
- // Check if the property was marked as remove in the cache or has been expired.
- if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow))
- {
- stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl);
- return true;
- }
+ //Add to the cache
+ cache.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add, ttl));
+ return addedToState;
+ }
- return false;
- }
+ public async Task GetStateAsync(string stateName, CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
- if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
- {
- return false;
- }
+ var condRes = await this.TryGetStateAsync(stateName, cancellationToken);
- stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl);
- return true;
+ if (condRes.HasValue)
+ {
+ return condRes.Value;
}
- public async Task GetStateAsync(string stateName, CancellationToken cancellationToken)
- {
- EnsureStateProviderInitialized();
+ throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName));
+ }
- var condRes = await this.TryGetStateAsync(stateName, cancellationToken);
+ public async Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- if (condRes.HasValue)
- {
- return condRes.Value;
- }
+ EnsureStateProviderInitialized();
- throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName));
+ var stateChangeTracker = GetContextualStateTracker();
+ var getCacheValue = stateChangeTracker.TryGet(stateName, out var state);
+ if (getCacheValue.containsKey)
+ {
+ return getCacheValue.isMarkedAsRemoveOrExpired
+ ? new ConditionalValue(false, default)
+ : new ConditionalValue(true, (T)state!.Value);
}
-
- public async Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken)
+
+ var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken);
+ if (conditionalResult.HasValue)
{
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
-
- EnsureStateProviderInitialized();
+ var stateMetadata = ActorStateCache.StateMetadata.Create(conditionalResult.Value.Value,
+ StateChangeKind.None, conditionalResult.Value.TTLExpireTime);
+ stateChangeTracker.Add(stateName, stateMetadata);
+ return new ConditionalValue(true, conditionalResult.Value.Value);
+ }
- var stateChangeTracker = GetContextualStateTracker();
+ return new ConditionalValue(false, default);
+ }
- if (stateChangeTracker.ContainsKey(stateName))
- {
- var stateMetadata = stateChangeTracker[stateName];
+ public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- // Check if the property was marked as remove in the cache or is expired
- if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow))
- {
- return new ConditionalValue(false, default);
- }
+ EnsureStateProviderInitialized();
- return new ConditionalValue(true, (T)stateMetadata.Value);
- }
-
- var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken);
- if (conditionalResult.HasValue)
+ var stateChangeTracker = GetContextualStateTracker();
+ var (cacheContainsKey, _) = stateChangeTracker.TryGet(stateName, out var state);
+ if (cacheContainsKey && state is not null)
+ {
+ var updatedState = state with { Value = value, TTLExpireTime = null };
+ if (state.ChangeKind is StateChangeKind.None or StateChangeKind.Remove)
{
- stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value.Value, StateChangeKind.None, ttlExpireTime: conditionalResult.Value.TTLExpireTime));
- return new ConditionalValue(true, conditionalResult.Value.Value);
+ updatedState = updatedState with { ChangeKind = StateChangeKind.Update };
}
- return new ConditionalValue(false, default);
+ stateChangeTracker.Set(stateName, updatedState);
}
-
- public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken)
+ else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(),
+ stateName, cancellationToken))
{
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
-
- EnsureStateProviderInitialized();
-
- var stateChangeTracker = GetContextualStateTracker();
-
- if (stateChangeTracker.ContainsKey(stateName))
- {
- var stateMetadata = stateChangeTracker[stateName];
- stateMetadata.Value = value;
- stateMetadata.TTLExpireTime = null;
-
- if (stateMetadata.ChangeKind == StateChangeKind.None ||
- stateMetadata.ChangeKind == StateChangeKind.Remove)
- {
- stateMetadata.ChangeKind = StateChangeKind.Update;
- }
- }
- else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
- {
- stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update));
- }
- else
- {
- stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add);
- }
+ stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Update));
}
-
- public async Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken)
+ else
{
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add));
+ }
+ }
- EnsureStateProviderInitialized();
+ public async Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- var stateChangeTracker = GetContextualStateTracker();
+ EnsureStateProviderInitialized();
- if (stateChangeTracker.ContainsKey(stateName))
- {
- var stateMetadata = stateChangeTracker[stateName];
- stateMetadata.Value = value;
- stateMetadata.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl);
-
- if (stateMetadata.ChangeKind == StateChangeKind.None ||
- stateMetadata.ChangeKind == StateChangeKind.Remove)
- {
- stateMetadata.ChangeKind = StateChangeKind.Update;
- }
- }
- else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
- {
- stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl));
- }
- else
+ var stateChangeTracker = GetContextualStateTracker();
+ var getCacheValue = stateChangeTracker.TryGet(stateName, out var state);
+ if (getCacheValue.containsKey && state is not null)
+ {
+ var updatedState = state with { Value = state.Value, TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl) };
+ if (updatedState.ChangeKind is StateChangeKind.None or StateChangeKind.Remove)
{
- stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl);
+ updatedState = updatedState with { ChangeKind = StateChangeKind.Update };
}
+ stateChangeTracker.Set(stateName, updatedState);
}
-
- public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken)
+ else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(),
+ stateName, cancellationToken))
{
- EnsureStateProviderInitialized();
-
- if (!(await this.TryRemoveStateAsync(stateName, cancellationToken)))
- {
- throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName));
- }
+ stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Update, ttl));
}
-
- public async Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken)
+ else
{
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
-
- EnsureStateProviderInitialized();
-
- var stateChangeTracker = GetContextualStateTracker();
-
- if (stateChangeTracker.ContainsKey(stateName))
- {
- var stateMetadata = stateChangeTracker[stateName];
-
- if (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)
- {
- stateChangeTracker.Remove(stateName);
- return false;
- }
-
- switch (stateMetadata.ChangeKind)
- {
- case StateChangeKind.Remove:
- return false;
- case StateChangeKind.Add:
- stateChangeTracker.Remove(stateName);
- return true;
- }
-
- stateMetadata.ChangeKind = StateChangeKind.Remove;
- return true;
- }
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add, ttl));
+ }
+ }
- if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
- {
- stateChangeTracker.Add(stateName, StateMetadata.CreateForRemove());
- return true;
- }
+ public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
- return false;
+ if (!(await this.TryRemoveStateAsync(stateName, cancellationToken)))
+ {
+ throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName));
}
+ }
- public async Task ContainsStateAsync(string stateName, CancellationToken cancellationToken)
- {
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
+ public async Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- EnsureStateProviderInitialized();
+ EnsureStateProviderInitialized();
- var stateChangeTracker = GetContextualStateTracker();
+ var stateChangeTracker = GetContextualStateTracker();
- if (stateChangeTracker.ContainsKey(stateName))
+ var cacheGetResult = stateChangeTracker.TryGet(stateName, out var state);
+ if (cacheGetResult.containsKey && state is not null)
+ {
+ if (cacheGetResult.isMarkedAsRemoveOrExpired)
{
- var stateMetadata = stateChangeTracker[stateName];
-
- // Check if the property was marked as remove in the cache
- return stateMetadata.ChangeKind != StateChangeKind.Remove;
+ stateChangeTracker.Remove(stateName);
+ return false;
}
- if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
+ switch (state.ChangeKind)
{
- return true;
+ case StateChangeKind.Remove:
+ return false;
+ case StateChangeKind.Add:
+ stateChangeTracker.Remove(stateName);
+ return true;
}
- return false;
+ var updatedState = state with { ChangeKind = StateChangeKind.Remove };
+ stateChangeTracker.Set(stateName, updatedState);
+ return true;
}
-
- public async Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken)
+
+ if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
{
- EnsureStateProviderInitialized();
+ stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.CreateForRemove());
+ return true;
+ }
- var condRes = await this.TryGetStateAsync(stateName, cancellationToken);
+ return false;
+ }
- if (condRes.HasValue)
- {
- return condRes.Value;
- }
+ public async Task ContainsStateAsync(string stateName, CancellationToken cancellationToken)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add;
+ EnsureStateProviderInitialized();
- var stateChangeTracker = GetContextualStateTracker();
- stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind);
- return value;
+ var stateChangeTracker = GetContextualStateTracker();
+ var getCacheValue = stateChangeTracker.TryGet(stateName, out var state);
+ if (getCacheValue.containsKey && state is not null)
+ {
+ //Check if the property was marked as remove in the cache
+ return state.ChangeKind != StateChangeKind.Remove;
}
- public async Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken)
+ if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken))
{
- EnsureStateProviderInitialized();
-
- var condRes = await this.TryGetStateAsync(stateName, cancellationToken);
+ return true;
+ }
- if (condRes.HasValue)
- {
- return condRes.Value;
- }
+ return false;
+ }
- var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add;
+ public async Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
- var stateChangeTracker = GetContextualStateTracker();
- stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind, ttl: ttl);
- return value;
- }
+ var condRes = await this.TryGetStateAsync(stateName, cancellationToken);
- public async Task AddOrUpdateStateAsync(
- string stateName,
- T addValue,
- Func updateValueFactory,
- CancellationToken cancellationToken = default)
+ if (condRes.HasValue)
{
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
-
- EnsureStateProviderInitialized();
+ return condRes.Value;
+ }
- var stateChangeTracker = GetContextualStateTracker();
+ var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add;
- if (stateChangeTracker.ContainsKey(stateName))
- {
- var stateMetadata = stateChangeTracker[stateName];
+ var stateChangeTracker = GetContextualStateTracker();
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, changeKind));
+ return value;
+ }
- // Check if the property was marked as remove in the cache
- if (stateMetadata.ChangeKind == StateChangeKind.Remove)
- {
- stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update);
- return addValue;
- }
+ public async Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
- var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value);
- stateMetadata.Value = newValue;
+ var condRes = await this.TryGetStateAsync(stateName, cancellationToken);
- if (stateMetadata.ChangeKind == StateChangeKind.None)
- {
- stateMetadata.ChangeKind = StateChangeKind.Update;
- }
+ if (condRes.HasValue)
+ {
+ return condRes.Value;
+ }
- return newValue;
- }
+ var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add;
- var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken);
- if (conditionalResult.HasValue)
- {
- var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value);
- stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update));
+ var stateChangeTracker = GetContextualStateTracker();
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, changeKind, ttl));
+ return value;
+ }
- return newValue;
- }
+ public async Task AddOrUpdateStateAsync(
+ string stateName,
+ T addValue,
+ Func updateValueFactory,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add);
- return addValue;
- }
+ EnsureStateProviderInitialized();
- public async Task AddOrUpdateStateAsync(
- string stateName,
- T addValue,
- Func updateValueFactory,
- TimeSpan ttl,
- CancellationToken cancellationToken = default)
+ var stateChangeTracker = GetContextualStateTracker();
+ var getCacheValue = stateChangeTracker.TryGet(stateName, out var state);
+ if (getCacheValue.containsKey && state is not null)
{
- ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
-
- EnsureStateProviderInitialized();
-
- var stateChangeTracker = GetContextualStateTracker();
-
- if (stateChangeTracker.ContainsKey(stateName))
+ //Check if the property was marked as remove in the cache
+ if (state.ChangeKind == StateChangeKind.Remove)
{
- var stateMetadata = stateChangeTracker[stateName];
-
- // Check if the property was marked as remove in the cache
- if (stateMetadata.ChangeKind == StateChangeKind.Remove)
- {
- stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update, ttl: ttl);
- return addValue;
- }
-
- var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value);
- stateMetadata.Value = newValue;
-
- if (stateMetadata.ChangeKind == StateChangeKind.None)
- {
- stateMetadata.ChangeKind = StateChangeKind.Update;
- }
-
- return newValue;
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Update));
+ return addValue;
}
- var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken);
- if (conditionalResult.HasValue)
- {
- var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value);
- stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update, ttl: ttl));
+ var newValue = updateValueFactory.Invoke(stateName, (T)state.Value);
+ var updatedState = state with { Value = newValue };
- return newValue;
+ if (state.ChangeKind == StateChangeKind.None)
+ {
+ updatedState = updatedState with { ChangeKind = StateChangeKind.Update };
}
-
- stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add, ttl: ttl);
- return addValue;
+
+ stateChangeTracker.Set(stateName, updatedState);
+ return newValue;
}
- public Task ClearCacheAsync(CancellationToken cancellationToken)
+ var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken);
+ if (conditionalResult.HasValue)
{
- EnsureStateProviderInitialized();
+ var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value);
+ stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(newValue, StateChangeKind.Update));
- var stateChangeTracker = GetContextualStateTracker();
-
- stateChangeTracker.Clear();
- return Task.CompletedTask;
+ return newValue;
}
- public async Task SaveStateAsync(CancellationToken cancellationToken = default)
- {
- EnsureStateProviderInitialized();
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Add));
+ return addValue;
+ }
- var stateChangeTracker = GetContextualStateTracker();
+ public async Task AddOrUpdateStateAsync(
+ string stateName,
+ T addValue,
+ Func updateValueFactory,
+ TimeSpan ttl,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
- if (stateChangeTracker.Count > 0)
- {
- var stateChangeList = new List();
- var statesToRemove = new List();
-
- foreach (var stateName in stateChangeTracker.Keys)
- {
- var stateMetadata = stateChangeTracker[stateName];
-
- if (stateMetadata.ChangeKind != StateChangeKind.None)
- {
- stateChangeList.Add(
- new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind, stateMetadata.TTLExpireTime));
-
- if (stateMetadata.ChangeKind == StateChangeKind.Remove)
- {
- statesToRemove.Add(stateName);
- }
-
- // Mark the states as unmodified so that tracking for next invocation is done correctly.
- stateMetadata.ChangeKind = StateChangeKind.None;
- }
- }
-
- if (stateChangeList.Count > 0)
- {
- await this.actor.Host.StateProvider.SaveStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateChangeList.AsReadOnly(), cancellationToken);
- }
-
- // Remove the states from tracker whcih were marked for removal.
- foreach (var stateToRemove in statesToRemove)
- {
- stateChangeTracker.Remove(stateToRemove);
- }
- }
- }
+ EnsureStateProviderInitialized();
- public Task SetStateContext(string stateContext)
+ var stateChangeTracker = GetContextualStateTracker();
+ var getCacheValue = stateChangeTracker.TryGet(stateName, out var state);
+ if (getCacheValue.containsKey && state is not null)
{
- if (stateContext != null)
+ if (state.ChangeKind == StateChangeKind.Remove)
{
- context.Value = (stateContext, new Dictionary());
- }
- else
- {
- context.Value = (null, null);
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Update, ttl));
+ return addValue;
}
- return Task.CompletedTask;
- }
+ var newValue = updateValueFactory.Invoke(stateName, (T)state.Value);
+ var updatedState = state with { Value = newValue };
- private bool IsStateMarkedForRemove(string stateName)
- {
- var stateChangeTracker = GetContextualStateTracker();
-
- if (stateChangeTracker.ContainsKey(stateName) &&
- stateChangeTracker[stateName].ChangeKind == StateChangeKind.Remove)
+ if (state.ChangeKind == StateChangeKind.None)
{
- return true;
+ updatedState = updatedState with { ChangeKind = StateChangeKind.Update };
}
+
+ stateChangeTracker.Set(stateName, updatedState);
- return false;
+ return newValue;
}
- private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken)
+ var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken);
+ if (conditionalResult.HasValue)
{
- EnsureStateProviderInitialized();
- return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken);
- }
+ var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value);
+ stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(newValue, StateChangeKind.Update, ttl));
- private void EnsureStateProviderInitialized()
- {
- if (this.actor.Host.StateProvider == null)
- {
- throw new InvalidOperationException(
- "The actor was initialized without a state provider, and so cannot interact with state. " +
- "If this is inside a unit test, replace Actor.StateProvider with a mock.");
- }
+ return newValue;
}
- private Dictionary GetContextualStateTracker()
- {
- if (context.Value.id != null)
- {
- return context.Value.tracker;
- }
- else
- {
- return defaultTracker;
- }
- }
-
- private sealed class StateMetadata
- {
- private StateMetadata(object value, Type type, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime = null, TimeSpan? ttl = null)
- {
- this.Value = value;
- this.Type = type;
- this.ChangeKind = changeKind;
-
- if (ttlExpireTime.HasValue && ttl.HasValue) {
- throw new ArgumentException("Cannot specify both TTLExpireTime and TTL");
- }
- if (ttl.HasValue) {
- this.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl.Value);
- } else {
- this.TTLExpireTime = ttlExpireTime;
- }
- }
+ stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Add, ttl));
+ return addValue;
+ }
- public object Value { get; set; }
+ public Task ClearCacheAsync(CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
- public StateChangeKind ChangeKind { get; set; }
+ var cache = GetContextualStateTracker();
+ cache.Clear();
+
+ return Task.CompletedTask;
+ }
- public Type Type { get; }
+ public async Task SaveStateAsync(CancellationToken cancellationToken = default)
+ {
+ EnsureStateProviderInitialized();
- public DateTimeOffset? TTLExpireTime { get; set; }
+ var stateChangeTracker = GetContextualStateTracker();
+ var (stateChanges, statesToRemove) = stateChangeTracker.BuildChangeList();
- public static StateMetadata Create(T value, StateChangeKind changeKind)
+ if (stateChanges.Count > 0)
+ {
+ await this.actor.Host.StateProvider.SaveStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateChanges, cancellationToken);
+ }
+
+ //Remove the states from the tracker which were marked for removal
+ if (statesToRemove.Count > 0)
+ {
+ foreach (var stateToRemove in statesToRemove)
{
- return new StateMetadata(value, typeof(T), changeKind);
+ stateChangeTracker.Remove(stateToRemove);
}
+ }
+ }
- public static StateMetadata Create(T value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime)
- {
- return new StateMetadata(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime);
- }
+ public Task SetStateContext(string stateContext)
+ {
+ context.Value = stateContext != null ? (stateContext, new ActorStateCache()) : (null, null);
+ return Task.CompletedTask;
+ }
- public static StateMetadata Create(T value, StateChangeKind changeKind, TimeSpan? ttl)
- {
- return new StateMetadata(value, typeof(T), changeKind, ttl: ttl);
- }
+ private bool IsStateMarkedForRemove(string stateName)
+ {
+ var stateChangeTracker = GetContextualStateTracker();
- public static StateMetadata CreateForRemove()
- {
- return new StateMetadata(null, typeof(object), StateChangeKind.Remove);
- }
+ var getCacheResult = stateChangeTracker.TryGet(stateName, out var state);
+ return getCacheResult.containsKey && state is not null && state.ChangeKind == StateChangeKind.Remove;
+ }
+
+ private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken)
+ {
+ EnsureStateProviderInitialized();
+ return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken);
+ }
+
+ private void EnsureStateProviderInitialized()
+ {
+ if (this.actor.Host.StateProvider == null)
+ {
+ throw new InvalidOperationException(
+ "The actor was initialized without a state provider, and so cannot interact with state. " +
+ "If this is inside a unit test, replace Actor.StateProvider with a mock.");
}
}
+
+ private IActorStateCache GetContextualStateTracker() => context.Value.id != null ? context.Value.stateCache : defaultCache;
}
diff --git a/src/Dapr.Actors/Runtime/IActorStateCache.cs b/src/Dapr.Actors/Runtime/IActorStateCache.cs
new file mode 100644
index 000000000..10439ae8c
--- /dev/null
+++ b/src/Dapr.Actors/Runtime/IActorStateCache.cs
@@ -0,0 +1,85 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+#nullable enable
+using System;
+using System.Collections.Generic;
+
+namespace Dapr.Actors.Runtime;
+
+internal interface IActorStateCache
+{
+ ///
+ /// Adds the indicated value to the cache.
+ ///
+ /// The name of the state.
+ /// The cached value.
+ /// How far out the TTL expiry should be.
+ /// The type of value getting cached.
+ /// stateContainsKey indicates if the cache already contains the key or not and
+ /// addedToState indicates if the value was added or updated in the cache.
+ (bool stateContainsKey, bool addedToState) Add(string stateName, T value, TimeSpan? ttl = null);
+
+ ///
+ /// Adds the indicated value to the cache.
+ ///
+ /// The name of the state.
+ /// The cached value.
+ /// The TTL expiry timestamp.
+ /// The type of value getting cached.
+ /// stateContainsKey indicates if the cache already contains the key or not and
+ /// addedToState indicates if the value was added or updated in the cache.
+ (bool stateContainsKey, bool addedToState) Add(string stateName, T value, DateTimeOffset ttlExpiry);
+
+ ///
+ /// Sets the cache with the specified value whether it already exists or not.
+ ///
+ /// The name of the state to save the value to.
+ /// The state metadata to save to the cache.
+ void Set(string stateName, ActorStateCache.StateMetadata metadata);
+
+ ///
+ /// Removes the indicated state name from the cache.
+ ///
+ /// The name of the state to remove.
+ void Remove(string stateName);
+
+ ///
+ /// Retrieves the current state from the cache if available and not expired.
+ ///
+ /// The name of the state to retrieve.
+ /// If available and not expired, the value of the state persisted in the cache.
+ /// True if the cache contains the state name; false if not.
+ (bool containsKey, bool isMarkedAsRemoveOrExpired) TryGet(
+ string stateName,
+ out ActorStateCache.StateMetadata? metadata);
+
+ ///
+ /// Clears the all the data from the cache.
+ ///
+ void Clear();
+
+ ///
+ /// Builds out the change lists of states to update in the provider and states to remove from the cache. This
+ /// is typically only called by invocation of the SaveStateAsync method in .
+ ///
+ ///
+ (IReadOnlyList stateChanges, IReadOnlyList statesToRemove) BuildChangeList();
+
+ ///
+ /// Helper method that determines if a state metadata is expired.
+ ///
+ /// The metadata to evaluate.
+ /// True if the state metadata is marked for removal or the TTL has expired, otherwise false.
+ bool IsMarkedAsRemoveOrExpired(ActorStateCache.StateMetadata metadata);
+}
diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs
index a4e0e4140..402d7474f 100644
--- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs
+++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs
@@ -11,182 +11,203 @@
// limitations under the License.
// ------------------------------------------------------------------------
-namespace Dapr.Actors.Test
+namespace Dapr.Actors.Test;
+
+using System;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Xunit;
+using Dapr.Actors.Communication;
+using Dapr.Actors.Runtime;
+using Moq;
+
+///
+/// Contains tests for ActorStateManager.
+///
+public sealed class ActorStateManagerTest
{
- using System;
- using System.Text.Json;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Collections.Generic;
- using Xunit;
- using Dapr.Actors.Communication;
- using Dapr.Actors.Runtime;
- using Moq;
-
- ///
- /// Contains tests for ActorStateManager.
- ///
- public class ActorStateManagerTest
+ [Fact]
+ public async Task SetGet()
{
- [Fact]
- public async Task SetGet()
- {
- var interactor = new Mock();
- var host = ActorHost.CreateForTest();
- host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
- var mngr = new ActorStateManager(new TestActor(host));
- var token = new CancellationToken();
-
- interactor
- .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(new ActorStateResponse("", null)));
-
- await mngr.AddStateAsync("key1", "value1", token);
- await mngr.AddStateAsync("key2", "value2", token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
-
- await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token));
- await Assert.ThrowsAsync(() => mngr.AddStateAsync("key2", "value4", token));
-
- await mngr.SetStateAsync("key1", "value5", token);
- await mngr.SetStateAsync("key2", "value6", token);
- Assert.Equal("value5", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value6", await mngr.GetStateAsync("key2", token));
- }
-
- [Fact]
- public async Task StateWithTTL()
- {
- var interactor = new Mock();
- var host = ActorHost.CreateForTest();
- host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
- var mngr = new ActorStateManager(new TestActor(host));
- var token = new CancellationToken();
-
- interactor
- .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(new ActorStateResponse("", null)));
-
- await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token);
- await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
-
- await Task.Delay(TimeSpan.FromSeconds(1.5));
-
- await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token));
- await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token));
-
- // Should be able to add state again after expiry and should not expire.
- await mngr.AddStateAsync("key1", "value1", token);
- await mngr.AddStateAsync("key2", "value2", token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
- await Task.Delay(TimeSpan.FromSeconds(1.5));
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
- }
-
- [Fact]
- public async Task StateRemoveAddTTL()
- {
- var interactor = new Mock();
- var host = ActorHost.CreateForTest();
- host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
- var mngr = new ActorStateManager(new TestActor(host));
- var token = new CancellationToken();
-
- interactor
- .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(new ActorStateResponse("", null)));
-
- await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token);
- await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
-
- await mngr.SetStateAsync("key1", "value1", token);
- await mngr.SetStateAsync("key2", "value2", token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
-
- // TTL is removed so state should not expire.
- await Task.Delay(TimeSpan.FromSeconds(1.5));
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
-
- // Adding TTL back should expire state.
- await mngr.SetStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token);
- await mngr.SetStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
- await Task.Delay(TimeSpan.FromSeconds(1.5));
- await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token));
- await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token));
- }
-
- [Fact]
- public async Task StateDaprdExpireTime()
- {
- var interactor = new Mock();
- var host = ActorHost.CreateForTest();
- host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
- var mngr = new ActorStateManager(new TestActor(host));
- var token = new CancellationToken();
-
- // Existing key which has an expiry time.
- interactor
- .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(new ActorStateResponse("\"value1\"", DateTime.UtcNow.AddSeconds(1))));
-
- await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token));
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
-
- // No longer return the value from the state provider.
- interactor
- .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(new ActorStateResponse("", null)));
-
- // Key should be expired after 1 seconds.
- await Task.Delay(TimeSpan.FromSeconds(1.5));
- await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token));
- await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token));
- await mngr.AddStateAsync("key1", "value2", TimeSpan.FromSeconds(1), token);
- Assert.Equal("value2", await mngr.GetStateAsync("key1", token));
- }
-
- [Fact]
- public async Task RemoveState()
- {
- var interactor = new Mock();
- var host = ActorHost.CreateForTest();
- host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
- var mngr = new ActorStateManager(new TestActor(host));
- var token = new CancellationToken();
-
- interactor
- .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(new ActorStateResponse("", null)));
-
- await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token));
-
- await mngr.AddStateAsync("key1", "value1", token);
- await mngr.AddStateAsync("key2", "value2", token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
-
- await mngr.RemoveStateAsync("key1", token);
- await mngr.RemoveStateAsync("key2", token);
-
- await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token));
- await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token));
-
- // Should be able to add state again after removal.
- await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token);
- await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token);
- Assert.Equal("value1", await mngr.GetStateAsync("key1", token));
- Assert.Equal("value2", await mngr.GetStateAsync("key2", token));
- }
+ var interactor = new Mock();
+ var host = ActorHost.CreateForTest();
+ host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
+ var mngr = new ActorStateManager(new TestActor(host));
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ interactor
+ .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(new ActorStateResponse("", null)));
+
+ const string key1 = "key1";
+ const string key2 = "key2";
+ const string val1 = "value1";
+ const string val2 = "value2";
+ const string val3 = "value3";
+ const string val4 = "value4";
+ const string val5 = "value5";
+ const string val6 = "value6";
+
+ await mngr.AddStateAsync(key1, val1, cts.Token);
+ await mngr.AddStateAsync(key2, val2, cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+
+ await Assert.ThrowsAsync(() => mngr.AddStateAsync(key1, val3, cts.Token));
+ await Assert.ThrowsAsync(() => mngr.AddStateAsync(key2, val4, cts.Token));
+
+ await mngr.SetStateAsync(key1, val5, cts.Token);
+ await mngr.SetStateAsync(key2, val6, cts.Token);
+ Assert.Equal(val5, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val6, await mngr.GetStateAsync(key2, cts.Token));
+ }
+
+ [Fact]
+ public async Task StateWithTTL()
+ {
+ var interactor = new Mock();
+ var host = ActorHost.CreateForTest();
+ host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
+ var mngr = new ActorStateManager(new TestActor(host));
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ interactor
+ .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(new ActorStateResponse("", null)));
+
+ const string key1 = "key1";
+ const string key2 = "key2";
+ const string val1 = "value1";
+ const string val2 = "value2";
+
+ await mngr.AddStateAsync(key1, val1, TimeSpan.FromSeconds(1), cts.Token);
+ await mngr.AddStateAsync(key2, val2, TimeSpan.FromSeconds(1), cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+
+ await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token);
+
+ await Assert.ThrowsAsync(() => mngr.GetStateAsync(key1, cts.Token));
+ await Assert.ThrowsAsync(() => mngr.GetStateAsync(key2, cts.Token));
+
+ // Should be able to add state again after expiry and should not expire.
+ await mngr.AddStateAsync(key1, val1, cts.Token);
+ await mngr.AddStateAsync(key2, val2, cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+ await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+ }
+
+ [Fact]
+ public async Task StateRemoveAddTTL()
+ {
+ var interactor = new Mock();
+ var host = ActorHost.CreateForTest();
+ host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
+ var mngr = new ActorStateManager(new TestActor(host));
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
+
+ interactor
+ .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(new ActorStateResponse("", null)));
+
+ const string key1 = "key1";
+ const string key2 = "key2";
+ const string val1 = "value1";
+ const string val2 = "value2";
+
+ await mngr.AddStateAsync(key1, val1, TimeSpan.FromSeconds(1), cts.Token);
+ await mngr.AddStateAsync(key2, val2, TimeSpan.FromSeconds(1), cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+
+ await mngr.SetStateAsync(key1, val1, cts.Token);
+ await mngr.SetStateAsync(key2, val2, cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+
+ // TTL is removed so state should not expire.
+ await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+
+ // Adding TTL back should expire state.
+ await mngr.SetStateAsync(key1, val1, TimeSpan.FromSeconds(1), cts.Token);
+ await mngr.SetStateAsync(key2, val2, TimeSpan.FromSeconds(1), cts.Token);
+ Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token));
+ Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token));
+ await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token);
+ await Assert.ThrowsAsync(() => mngr.GetStateAsync(key1, cts.Token));
+ await Assert.ThrowsAsync(() => mngr.GetStateAsync(key2, cts.Token));
+ }
+
+ [Fact]
+ public async Task ValidateStateExpirationAndExceptions()
+ {
+ var interactor = new Mock();
+ var host = ActorHost.CreateForTest();
+ host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions());
+ var mngr = new ActorStateManager(new TestActor(host));
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
+
+ // Existing key which has an expiry time of 1 second - this is triggered on the call to `this.actor.Host.StateProvider.ContainsStateAsync` during `mngr.AddStateAsync`
+ interactor
+ .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(),
+ It.IsAny()))
+ .Returns(Task.FromResult(new ActorStateResponse("\"value1\"", DateTime.UtcNow.AddSeconds(1))));
+
+ await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", TimeSpan.FromSeconds(1), cts.Token)); //This is placed before the interactor runs as cache is checked first
+ Assert.Equal("value3", await mngr.GetStateAsync("key1", cts.Token)); //Validate against the cache value
+
+ // No longer return the value from the state provider.
+ interactor
+ .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(new ActorStateResponse("", null)));
+
+ // Key should be expired after 1 seconds.
+ await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token);
+
+ // While the key will be in the cache, it should no longer be valid as it expired after a second and we've delayed 1.5 seconds
+ await Assert.ThrowsAsync