microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
mehak/state

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Apps/State/StateScope.cs

126lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Text.Json;
5
6namespace Microsoft.Teams.Apps.State;
7
8/// <summary>
9/// A single state scope (conversation or user) — a string-keyed bag of values.
10/// </summary>
11/// <remarks>
12/// Persisted scopes (conversation, user) are change-tracked with a dirty flag: <see cref="Set{T}"/>,
13/// <see cref="Remove"/>, and <see cref="Clear"/> mark the scope dirty, so only mutated scopes are written
14/// and a read-only turn writes nothing. Reads never mark a scope dirty; persist a mutation to a value
15/// fetched via <see cref="Get{T}"/> by writing it back with <see cref="Set{T}"/>.
16/// After the owning turn completes, every read and write throws <see cref="InvalidOperationException"/>
17/// (see <see cref="TurnState"/>).
18/// </remarks>
19public sealed class StateScope
20{
21 private readonly Dictionary<string, object?> _values;
22 private readonly bool _persisted;
23 private bool _dirty;
24 private bool _completed;
25
26 internal StateScope(bool persisted, IReadOnlyDictionary<string, object?>? loaded)
27 {
28 _persisted = persisted;
29 _values = loaded is not null ? new Dictionary<string, object?>(loaded) : [];
30 }
31
32 /// <summary>Gets the value stored under <paramref name="key"/>, or <c>default</c> if absent.</summary>
33 /// <typeparam name="T">The value type to read.</typeparam>
34 /// <param name="key">The value key.</param>
35 public T? Get<T>(string key)
36 {
37 ThrowIfCompleted();
38 ArgumentException.ThrowIfNullOrEmpty(key);
39
40 if (!_values.TryGetValue(key, out object? raw) || raw is null)
41 {
42 return default;
43 }
44
45 if (raw is T typed)
46 {
47 return typed;
48 }
49
50 if (raw is JsonElement element)
51 {
52 // Deserialize once per key per turn: cache the typed value for subsequent reads. Safe now
53 // that change tracking is a dirty flag — a read doesn't dirty the scope, so this can't cause
54 // a read-only turn to write. Persist a mutation to the returned value with Set.
55 T? converted = StateSerializer.Convert<T>(element);
56 _values[key] = converted;
57 return converted;
58 }
59
60 return default;
61 }
62
63 /// <summary>Stores <paramref name="value"/> under <paramref name="key"/>.</summary>
64 /// <typeparam name="T">The value type to store.</typeparam>
65 /// <param name="key">The value key.</param>
66 /// <param name="value">The value to store.</param>
67 public void Set<T>(string key, T value)
68 {
69 ThrowIfCompleted();
70 ArgumentException.ThrowIfNullOrEmpty(key);
71 _values[key] = value;
72 _dirty = true;
73 }
74
75 /// <summary>Removes the value stored under <paramref name="key"/>.</summary>
76 /// <param name="key">The value key.</param>
77 /// <returns><see langword="true"/> if a value was removed; otherwise <see langword="false"/>.</returns>
78 public bool Remove(string key)
79 {
80 ThrowIfCompleted();
81 ArgumentException.ThrowIfNullOrEmpty(key);
82 bool removed = _values.Remove(key);
83 _dirty |= removed;
84 return removed;
85 }
86
87 /// <summary>Whether a value is stored under <paramref name="key"/>.</summary>
88 /// <param name="key">The value key.</param>
89 public bool ContainsKey(string key)
90 {
91 ThrowIfCompleted();
92 ArgumentException.ThrowIfNullOrEmpty(key);
93 return _values.ContainsKey(key);
94 }
95
96 /// <summary>Removes every value from the scope. A persisted scope emptied this way is deleted from storage on save.</summary>
97 public void Clear()
98 {
99 ThrowIfCompleted();
100 _dirty |= _values.Count > 0;
101 _values.Clear();
102 }
103
104 /// <summary>True if the scope currently holds no values.</summary>
105 internal bool IsEmpty => _values.Count == 0;
106
107 /// <summary>True if this is a persisted scope that was mutated this turn (Set / Remove / Clear).</summary>
108 internal bool IsChanged() => _persisted && _dirty;
109
110 /// <summary>Snapshots the scope's values into a new dictionary for writing to storage.</summary>
111 internal Dictionary<string, object?> Snapshot() => new(_values);
112
113 /// <summary>Seals the scope; subsequent access throws.</summary>
114 internal void Complete() => _completed = true;
115
116 private void ThrowIfCompleted()
117 {
118 if (_completed)
119 {
120 throw new InvalidOperationException(
121 "TurnState was accessed after the turn completed. State is per-turn and is saved once " +
122 "when the handler returns. Read the values you need during the turn and pass them into " +
123 "any background work, e.g. `var name = ctx.State.User.Get<string>(\"name\");`.");
124 }
125 }
126}
127