microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feature/oauthflow-fixes

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

218lines · 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/// Per-turn state storage backed by a dictionary, supporting both key-value and typed object access.
10/// This type is not thread-safe; each instance is scoped to a single turn.
11/// </summary>
12public class TurnState
13{
14 private readonly Dictionary<string, object?> _data;
15
16 /// <summary>
17 /// Initializes a new, empty <see cref="TurnState"/>.
18 /// </summary>
19 public TurnState() => _data = [];
20
21 private TurnState(Dictionary<string, object?> data) => _data = data;
22
23 /// <summary>
24 /// Returns true if any value has been added, modified, or removed since the state was loaded.
25 /// </summary>
26 public bool IsDirty { get; internal set; }
27
28 /// <summary>
29 /// Gets a value by key. Returns <c>default</c> if the key is not present or the value cannot be converted.
30 /// </summary>
31 public T? Get<T>(string key)
32 {
33 if (!_data.TryGetValue(key, out object? value) || value is null)
34 {
35 return default;
36 }
37
38 if (value is T typed)
39 {
40 return typed;
41 }
42
43 // Handle JsonElement values from deserialization.
44 if (value is JsonElement element)
45 {
46 try
47 {
48 return element.Deserialize<T>();
49 }
50 catch (Exception ex) when (ex is JsonException or NotSupportedException)
51 {
52 return default;
53 }
54 }
55
56 return default;
57 }
58
59 /// <summary>
60 /// Sets a value by key.
61 /// </summary>
62 public void Set<T>(string key, T value)
63 {
64 _data[key] = value;
65 IsDirty = true;
66 }
67
68 /// <summary>
69 /// Removes a key from state.
70 /// </summary>
71 public void Remove(string key)
72 {
73 if (_data.Remove(key))
74 {
75 IsDirty = true;
76 }
77 }
78
79 /// <summary>
80 /// Attempts to get a value by key.
81 /// Returns <c>true</c> if the key exists and the value can be converted to <typeparamref name="T"/>.
82 /// </summary>
83 public bool TryGet<T>(string key, out T? value)
84 {
85 if (!_data.TryGetValue(key, out object? raw) || raw is null)
86 {
87 value = default;
88 return false;
89 }
90
91 if (raw is T typed)
92 {
93 value = typed;
94 return true;
95 }
96
97 if (raw is JsonElement element)
98 {
99 try
100 {
101 value = element.Deserialize<T>();
102 return value is not null;
103 }
104 catch (Exception ex) when (ex is JsonException or NotSupportedException)
105 {
106 value = default;
107 return false;
108 }
109 }
110
111 value = default;
112 return false;
113 }
114
115 /// <summary>
116 /// Returns <c>true</c> if the key exists in state.
117 /// </summary>
118 public bool ContainsKey(string key) => _data.ContainsKey(key);
119
120 /// <summary>
121 /// Gets a typed state object. Creates a new instance via parameterless constructor if not present.
122 /// </summary>
123 public T Get<T>() where T : class, new()
124 {
125 string key = TypeKey<T>();
126
127 if (_data.TryGetValue(key, out object? value) && value is not null)
128 {
129 if (value is T typed)
130 {
131 return typed;
132 }
133
134 if (value is JsonElement element)
135 {
136 try
137 {
138 T deserialized = element.Deserialize<T>() ?? new T();
139 _data[key] = deserialized;
140 IsDirty = true;
141 return deserialized;
142 }
143 catch (Exception ex) when (ex is JsonException or NotSupportedException)
144 {
145 // Fall through to create new instance
146 }
147 }
148 }
149
150 T instance = new();
151 _data[key] = instance;
152 IsDirty = true;
153 return instance;
154 }
155
156 /// <summary>
157 /// Sets a typed state object, replacing any existing instance of the same type.
158 /// </summary>
159 public void Set<T>(T value) where T : class
160 {
161 _data[TypeKey<T>()] = value;
162 IsDirty = true;
163 }
164
165 /// <summary>
166 /// Returns <c>true</c> if a typed state object of this type exists.
167 /// </summary>
168 public bool Has<T>() where T : class => _data.ContainsKey(TypeKey<T>());
169
170 /// <summary>
171 /// Removes the typed state object of this type.
172 /// </summary>
173 public void Remove<T>() where T : class
174 {
175 if (_data.Remove(TypeKey<T>()))
176 {
177 IsDirty = true;
178 }
179 }
180
181 /// <summary>
182 /// Serializes the state to a JSON byte array.
183 /// </summary>
184 public byte[] ToJsonBytes() => JsonSerializer.SerializeToUtf8Bytes(_data);
185
186 /// <summary>
187 /// Deserializes a <see cref="TurnState"/> from a JSON byte array.
188 /// Returns an empty, non-dirty state when <paramref name="bytes"/> is null or empty.
189 /// </summary>
190 public static TurnState FromJsonBytes(byte[]? bytes)
191 {
192 if (bytes is null || bytes.Length == 0)
193 {
194 return new TurnState();
195 }
196
197 try
198 {
199 Dictionary<string, object?>? data = JsonSerializer.Deserialize<Dictionary<string, object?>>(bytes);
200 return new TurnState(data ?? []);
201 }
202 catch (Exception ex) when (ex is JsonException or NotSupportedException)
203 {
204 // Treat corrupted cache payload as a cache miss.
205 return new TurnState();
206 }
207 }
208
209 /// <summary>
210 /// Creates a <see cref="TurnState"/> from an existing dictionary. Useful for testing.
211 /// </summary>
212 public static TurnState FromDictionary(Dictionary<string, object?> data)
213 {
214 return new TurnState(new Dictionary<string, object?>(data));
215 }
216
217 private static string TypeKey<T>() => $"${typeof(T).FullName}";
218}
219