diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 184812cab2..cc9ddcbdf4 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -37,6 +37,7 @@
+
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
index a8f0eaad05..2225921310 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
@@ -2,7 +2,6 @@
using System;
using System.IO;
-using System.Linq;
using Microsoft.Agents.Workflows.Declarative.Extensions;
using Microsoft.Agents.Workflows.Declarative.Interpreter;
using Microsoft.Agents.Workflows.Declarative.PowerFx;
@@ -22,7 +21,7 @@ public static class DeclarativeWorkflowBuilder
///
/// The type of the input message
/// The path to the workflow.
- /// The execution context for the workflow.
+ /// Configuration options for workflow execution.
/// An optional function to transform the input message into a .
///
public static Workflow Build(
@@ -40,7 +39,7 @@ public static class DeclarativeWorkflowBuilder
///
/// The type of the input message
/// The reader that provides the workflow object model YAML.
- /// The execution context for the workflow.
+ /// Configuration options for workflow execution.
/// An optional function to transform the input message into a .
/// The that corresponds with the YAML object model.
public static Workflow Build(
@@ -59,18 +58,18 @@ public static class DeclarativeWorkflowBuilder
string rootId = WorkflowActionVisitor.Steps.Root(workflowElement.BeginDialog?.Id.Value);
- WorkflowScopes scopes = new();
- scopes.Initialize(WrapWithBot(workflowElement), options.Configuration);
- DeclarativeWorkflowState state = new(options.CreateRecalcEngine(), scopes);
+ WorkflowFormulaState state = new(options.CreateRecalcEngine());
+ state.Initialize(workflowElement.WrapWithBot(), options.Configuration);
DeclarativeWorkflowExecutor rootExecutor =
new(rootId,
state,
message => inputTransform?.Invoke(message) ?? DefaultTransform(message));
WorkflowActionVisitor visitor = new(rootExecutor, state, options);
- WorkflowElementWalker walker = new(rootElement, visitor);
+ WorkflowElementWalker walker = new(visitor);
+ walker.Visit(rootElement);
- return walker.GetWorkflow();
+ return visitor.Complete();
}
private static ChatMessage DefaultTransform(object message) =>
@@ -80,23 +79,4 @@ public static class DeclarativeWorkflowBuilder
string stringMessage => new ChatMessage(ChatRole.User, stringMessage),
_ => new(ChatRole.User, $"{message}")
};
-
- // Wrap with bot to ensure schema is set.
- private static AdaptiveDialog WrapWithBot(AdaptiveDialog dialog)
- {
- BotDefinition bot
- = new BotDefinition.Builder
- {
- Components =
- {
- new DialogComponent.Builder
- {
- SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema",
- Dialog = new AdaptiveDialog.Builder(dialog),
- }
- }
- }.Build();
-
- return bot.Descendants().OfType().First();
- }
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
index 32de8a66a6..34e7efdb80 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
@@ -141,7 +141,7 @@ internal static class ChatMessageExtensions
}
AgentMessageRole? role = null;
- if (Enum.TryParse(roleValue.Value, out AgentMessageRole parsedRole))
+ if (Enum.TryParse(roleValue.Value, out AgentMessageRole parsedRole))
{
role = parsedRole;
}
@@ -188,13 +188,13 @@ internal static class ChatMessageExtensions
yield return new NamedValue(TypeSchema.Message.Fields.Id, message.MessageId.ToFormula());
yield return new NamedValue(TypeSchema.Message.Fields.Role, message.Role.Value.ToFormula());
yield return new NamedValue(TypeSchema.Message.Fields.Author, message.AuthorName.ToFormula());
- yield return new NamedValue(TypeSchema.Message.Fields.Content, TableValue.NewTable(s_contentRecordType, message.GetContentRecords()));
+ yield return new NamedValue(TypeSchema.Message.Fields.Content, FormulaValue.NewTable(s_contentRecordType, message.GetContentRecords()));
yield return new NamedValue(TypeSchema.Message.Fields.Text, message.Text.ToFormula());
yield return new NamedValue(TypeSchema.Message.Fields.Metadata, message.AdditionalProperties.ToRecord());
}
private static IEnumerable GetContentRecords(this ChatMessage message) =>
- message.Contents.Select(content => RecordValue.NewRecordFromFields(content.GetContentFields()));
+ message.Contents.Select(content => FormulaValue.NewRecordFromFields(content.GetContentFields()));
private static IEnumerable GetContentFields(this AIContent content)
{
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
index 687cc302f2..7fa82066e1 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Dynamic;
using System.Linq;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
@@ -59,13 +60,13 @@ internal static class DataValueExtensions
null => null,
BlankDataValue => null,
BooleanDataValue boolValue => boolValue.Value,
- NumberDataValue numberValue => (numberValue.Value),
- FloatDataValue floatValue => (floatValue.Value),
- StringDataValue stringValue => (stringValue.Value),
- DateTimeDataValue dateTimeValue => (dateTimeValue.Value.DateTime),
+ NumberDataValue numberValue => numberValue.Value,
+ FloatDataValue floatValue => floatValue.Value,
+ StringDataValue stringValue => stringValue.Value,
+ DateTimeDataValue dateTimeValue => dateTimeValue.Value.DateTime,
DateDataValue dateValue => dateValue.Value,
TimeDataValue timeValue => timeValue.Value,
- TableDataValue tableValue => tableValue.Values.Select(value => value.ToRecordValue()).ToArray(),
+ TableDataValue tableValue => tableValue.Values.Select(value => value.ToObject()).ToArray(),
RecordDataValue recordValue => recordValue.ToDictionary(),
OptionDataValue optionValue => optionValue.Value.Value,
_ => throw new DeclarativeModelException($"Unsupported {nameof(DataValue)} type: {value.GetType().Name}"),
@@ -88,6 +89,19 @@ internal static class DataValueExtensions
return recordType;
}
+ public static ExpandoObject ToObject(this RecordDataValue recordDataValue)
+ {
+ ExpandoObject expandoObject = new();
+
+ IDictionary dictionary = expandoObject;
+ foreach (KeyValuePair field in recordDataValue.Properties)
+ {
+ dictionary[field.Key] = field.Value?.ToObject();
+ }
+
+ return expandoObject;
+ }
+
private static RecordType ParseRecordType(this RecordDataValue record)
{
RecordType recordType = RecordType.Empty();
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs
new file mode 100644
index 0000000000..97b07068e7
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Linq;
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class DialogBaseExtensions
+{
+ public static TDialog WrapWithBot(this TDialog dialog) where TDialog : DialogBase
+ {
+ BotDefinition bot
+ = new BotDefinition.Builder
+ {
+ Components =
+ {
+ new DialogComponent.Builder
+ {
+ SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema",
+ Dialog = dialog.ToBuilder(),
+ }
+ }
+ }.Build();
+
+ return bot.Descendants().OfType().First();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
index 278a6b0ba9..f815486686 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
@@ -8,6 +8,7 @@ using System.Dynamic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using BlankType = Microsoft.PowerFx.Types.BlankType;
@@ -24,6 +25,7 @@ internal static class FormulaValueExtensions
value switch
{
null => FormulaValue.NewBlank(),
+ UnassignedValue => FormulaValue.NewBlank(),
FormulaValue formulaValue => formulaValue,
bool booleanValue => FormulaValue.New(booleanValue),
int decimalValue => FormulaValue.New(decimalValue),
@@ -142,7 +144,7 @@ internal static class FormulaValueExtensions
TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray());
public static RecordDataValue ToRecord(this RecordValue value) =>
- RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray());
+ RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()));
private static RecordValue ToRecord(this IDictionary value)
{
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
index fb4312ec42..aa6fc18a98 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
@@ -3,7 +3,9 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Declarative.Interpreter;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
+using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
@@ -17,4 +19,21 @@ internal static class IWorkflowContextExtensions
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) =>
context.SendMessageAsync(new ExecutorResultMessage(id, result));
+
+ public static ValueTask QueueStateUpdateAsync(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
+ context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName));
+
+ public static async Task GetStateAsync(this IWorkflowContext context, CancellationToken cancellationToken)
+ {
+ if (context is DeclarativeWorkflowContext declarativeContext)
+ {
+ return declarativeContext.State;
+ }
+
+ WorkflowFormulaState state = new(RecalcEngineFactory.Create());
+
+ await state.RestoreAsync(context, cancellationToken).ConfigureAwait(false);
+
+ return state;
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
index e8978347fd..44e845e63c 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
+using System.Collections.Frozen;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Declarative.Extensions;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -15,7 +15,7 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
-internal abstract class DeclarativeActionExecutor(TAction model, DeclarativeWorkflowState state) :
+internal abstract class DeclarativeActionExecutor(TAction model, WorkflowFormulaState state) :
DeclarativeActionExecutor(model, state)
where TAction : DialogAction
{
@@ -24,16 +24,15 @@ internal abstract class DeclarativeActionExecutor(TAction model, Declar
internal abstract class DeclarativeActionExecutor : Executor
{
- private static readonly ImmutableHashSet s_mutableScopes =
- new HashSet
- {
- VariableScopeNames.Topic,
- VariableScopeNames.Global,
- }.ToImmutableHashSet();
+ private static readonly FrozenSet s_mutableScopes =
+ [
+ VariableScopeNames.Topic,
+ VariableScopeNames.Global
+ ];
private string? _parentId;
- protected DeclarativeActionExecutor(DialogAction model, DeclarativeWorkflowState state)
+ protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState state)
: base(model.Id.Value)
{
if (!model.HasRequiredProperties)
@@ -51,7 +50,7 @@ internal abstract class DeclarativeActionExecutor : Executor.Instance;
- protected DeclarativeWorkflowState State { get; }
+ protected WorkflowFormulaState State { get; }
protected virtual bool IsDiscreteAction => true;
@@ -72,7 +71,7 @@ internal abstract class DeclarativeActionExecutor : Executor
+ public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent);
+
+ ///
+ public ValueTask QueueClearScopeAsync(string? scopeName = null)
+ {
+ this.State.ResetAll(scopeName);
+ return this.Source.QueueClearScopeAsync(scopeName);
+ }
+
+ ///
+ public async ValueTask QueueStateUpdateAsync(string key, T? value, string? scopeName = null)
+ {
+ ValueTask task = value switch
+ {
+ null => QueueEmptyStateAsync(),
+ FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue),
+ DataValue dataValue => QueueDataValueStateAsync(dataValue),
+ _ => QueueNativeStateAsync(value),
+ };
+
+ await task.ConfigureAwait(false);
+
+ ValueTask QueueEmptyStateAsync()
+ {
+ this.State.Set(key, FormulaValue.NewBlank(), scopeName);
+ return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName);
+ }
+
+ ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
+ {
+ this.State.Set(key, formulaValue, scopeName);
+ return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
+ }
+
+ ValueTask QueueDataValueStateAsync(DataValue dataValue)
+ {
+ FormulaValue formulaValue = dataValue.ToFormula();
+ this.State.Set(key, formulaValue, scopeName);
+ return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
+ }
+
+ ValueTask QueueNativeStateAsync(object? rawValue)
+ {
+ FormulaValue formulaValue = rawValue.ToFormula();
+ this.State.Set(key, formulaValue, scopeName);
+ return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
+ }
+ }
+
+ ///
+ public ValueTask ReadStateAsync(string key, string? scopeName = null) => this.Source.ReadStateAsync(key, scopeName);
+
+ ///
+ public ValueTask> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName);
+
+ ///
+ public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId);
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
index 9e6a39d50f..a912258157 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
@@ -12,15 +12,18 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
///
internal sealed class DeclarativeWorkflowExecutor(
string workflowId,
- DeclarativeWorkflowState state,
+ WorkflowFormulaState state,
Func inputTransform) :
Executor(workflowId)
where TInput : notnull
{
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context)
{
+ // No state to restore if we're starting from the beginning.
+ state.SetInitialized();
+
ChatMessage input = inputTransform.Invoke(message);
- await state.SetLastMessageAsync(context, input).ConfigureAwait(false);
+ state.SetLastMessage(input);
await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false);
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
index 3bff3ad5db..b78c935020 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
@@ -2,8 +2,8 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Diagnostics;
+using System.Linq;
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
@@ -89,7 +89,8 @@ internal sealed class DeclarativeWorkflowModel
public void ConnectNodes(WorkflowBuilder workflowBuilder)
{
- foreach (ModelNode node in this.Nodes.Values.ToImmutableArray())
+ // Push `Values` into array to avoid modification during iteration.
+ foreach (ModelNode node in this.Nodes.Values.ToArray())
{
if (node.CompletionHandler is not null)
{
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs
deleted file mode 100644
index adcfba8798..0000000000
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Agents.Workflows.Declarative.Extensions;
-using Microsoft.Agents.Workflows.Declarative.PowerFx;
-using Microsoft.Bot.ObjectModel;
-using Microsoft.PowerFx;
-using Microsoft.PowerFx.Types;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
-
-internal sealed class DeclarativeWorkflowState
-{
- private static readonly ImmutableHashSet s_mutableScopes =
- new HashSet
- {
- VariableScopeNames.Topic,
- VariableScopeNames.Global,
- VariableScopeNames.System,
- }.ToImmutableHashSet();
-
- private readonly RecalcEngine _engine;
- private readonly WorkflowScopes _scopes;
- private WorkflowExpressionEngine? _expressionEngine;
- private int _isInitialized;
-
- public DeclarativeWorkflowState(RecalcEngine engine, WorkflowScopes? scopes = null)
- {
- this._scopes = scopes ?? new WorkflowScopes();
- this._engine = engine;
- this._scopes.Bind(this._engine);
- }
-
- public WorkflowExpressionEngine ExpressionEngine => this._expressionEngine ??= new WorkflowExpressionEngine(this._engine);
-
- public void Reset(PropertyPath variablePath) =>
- this.Reset(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName));
-
- public void Reset(string scopeName, string? varName = null)
- {
- if (string.IsNullOrWhiteSpace(varName))
- {
- this._scopes.Clear(scopeName);
- }
- else
- {
- this._scopes.Reset(varName, scopeName);
- }
-
- this._scopes.Bind(this._engine, scopeName);
- }
-
- public FormulaValue Get(PropertyPath variablePath) =>
- this.Get(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName));
-
- public FormulaValue Get(string scope, string varName) =>
- this._scopes.Get(varName, scope);
-
- public ValueTask SetAsync(PropertyPath variablePath, FormulaValue value, IWorkflowContext context) =>
- this.SetAsync(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName), value, context);
-
- public async ValueTask SetAsync(string scopeName, string varName, FormulaValue value, IWorkflowContext context)
- {
- if (!s_mutableScopes.Contains(scopeName))
- {
- throw new DeclarativeModelException($"Invalid scope: {scopeName}");
- }
-
- this._scopes.Set(varName, value, scopeName);
- this._scopes.Bind(this._engine, scopeName);
-
- await context.QueueStateUpdateAsync(varName, value.ToObject(), scopeName).ConfigureAwait(false);
- }
-
- public string Format(IEnumerable template) => this._engine.Format(template);
-
- public string Format(TemplateLine? line) => this._engine.Format(line);
-
- public async ValueTask RestoreAsync(IWorkflowContext context, CancellationToken cancellationToken)
- {
- if (Interlocked.CompareExchange(ref this._isInitialized, 1, 0) == 1)
- {
- return;
- }
-
- await Task.WhenAll(s_mutableScopes.Select(scopeName => ReadScopeAsync(scopeName).AsTask())).ConfigureAwait(false);
-
- async ValueTask ReadScopeAsync(string scopeName)
- {
- HashSet keys = await context.ReadStateKeysAsync(scopeName).ConfigureAwait(false);
- foreach (string key in keys)
- {
- object? value = await context.ReadStateAsync