microsoft/teams.net

Public

mirrored fromhttps://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/TurnStateLoader.cs

170lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Diagnostics;
5using Microsoft.Extensions.Caching.Distributed;
6using Microsoft.Extensions.Logging;
7using Microsoft.Extensions.Options;
8using Microsoft.Teams.Apps.Diagnostics;
9using Microsoft.Teams.Core.Diagnostics;
10
11namespace Microsoft.Teams.Apps.State;
12
13/// <summary>
14/// Loads and saves per-turn state from a distributed cache.
15/// Manages two state scopes: conversation-scoped and user-scoped.
16/// </summary>
17public sealed class TurnStateLoader
18{
19 private readonly IDistributedCache _cache;
20 private readonly TurnStateOptions _options;
21 private readonly ILogger<TurnStateLoader> _logger;
22
23 /// <summary>
24 /// Initializes a new instance of the <see cref="TurnStateLoader"/> class.
25 /// </summary>
26 /// <param name="cache">The distributed cache used to persist turn state.</param>
27 /// <param name="options">Options controlling cache entry lifetime.</param>
28 /// <param name="logger">Logger for diagnostics.</param>
29 public TurnStateLoader(IDistributedCache cache, IOptions<TurnStateOptions> options, ILogger<TurnStateLoader> logger)
30 {
31 ArgumentNullException.ThrowIfNull(cache);
32 ArgumentNullException.ThrowIfNull(options);
33 _cache = cache;
34 _options = options.Value;
35 _logger = logger;
36
37 if (cache is MemoryDistributedCache)
38 {
39 logger.StateUsingInMemoryCache();
40 }
41 }
42
43 /// <summary>
44 /// Loads conversation and user state from the cache.
45 /// </summary>
46 public async Task<TurnStateContainer> LoadAsync(string conversationId, string? userId, CancellationToken cancellationToken)
47 {
48 using Activity? span = AppsTelemetry.Source.StartActivity(AppsTelemetry.Spans.StateLoad, ActivityKind.Internal);
49 long startTs = Stopwatch.GetTimestamp();
50
51 try
52 {
53 string conversationKey = $"{_options.KeyPrefix}:conv:{conversationId}";
54 byte[]? convBytes = await _cache.GetAsync(conversationKey, cancellationToken).ConfigureAwait(false);
55 TurnState conversationState = TurnState.FromJsonBytes(convBytes);
56
57 byte[]? userBytes = null;
58 TurnState? userState = null;
59 if (!string.IsNullOrEmpty(userId))
60 {
61 string userKey = $"{_options.KeyPrefix}:user:{conversationId}:{userId}";
62 userBytes = await _cache.GetAsync(userKey, cancellationToken).ConfigureAwait(false);
63 userState = TurnState.FromJsonBytes(userBytes);
64 }
65
66 long bytesRead = (convBytes?.Length ?? 0) + (userBytes?.Length ?? 0);
67 span?.SetTag(AppsTelemetry.Tags.StateConversationHit, convBytes is not null);
68 span?.SetTag(AppsTelemetry.Tags.StateUserHit, userBytes is not null);
69 span?.SetTag(AppsTelemetry.Tags.StateBytesRead, bytesRead);
70 AppsTelemetry.StateBytesRead.Record(bytesRead);
71 _logger.StateLoaded(conversationId, convBytes is not null, userBytes is not null);
72
73 return new TurnStateContainer(conversationState, userState);
74 }
75 catch (Exception ex)
76 {
77 span?.RecordException(ex);
78 AppsTelemetry.StateCacheErrors.Add(1, new KeyValuePair<string, object?>(AppsTelemetry.Tags.Operation, "load"));
79 _logger.StateLoadFailed(ex, conversationId);
80 throw;
81 }
82 finally
83 {
84 AppsTelemetry.StateLoadDuration.Record(Stopwatch.GetElapsedTime(startTs).TotalMilliseconds);
85 }
86 }
87
88 /// <summary>
89 /// Saves dirty state back to the cache.
90 /// </summary>
91 public async Task SaveAsync(TurnStateContainer container, string conversationId, string? userId, CancellationToken cancellationToken)
92 {
93 ArgumentNullException.ThrowIfNull(container);
94
95 using Activity? span = AppsTelemetry.Source.StartActivity(AppsTelemetry.Spans.StateSave, ActivityKind.Internal);
96 long startTs = Stopwatch.GetTimestamp();
97
98 try
99 {
100 bool convDirty = container.ConversationState.IsDirty;
101 bool userDirty = !string.IsNullOrEmpty(userId) && container.UserState is not null && container.UserState.IsDirty;
102 long bytesWritten = 0;
103
104 if (convDirty)
105 {
106 string conversationKey = $"{_options.KeyPrefix}:conv:{conversationId}";
107 byte[] bytes = container.ConversationState.ToJsonBytes();
108 bytesWritten += bytes.Length;
109 await _cache.SetAsync(conversationKey, bytes, _options.CacheEntryOptions, cancellationToken).ConfigureAwait(false);
110 }
111
112 if (userDirty)
113 {
114 string userKey = $"{_options.KeyPrefix}:user:{conversationId}:{userId}";
115 byte[] bytes = container.UserState!.ToJsonBytes();
116 bytesWritten += bytes.Length;
117 await _cache.SetAsync(userKey, bytes, _options.CacheEntryOptions, cancellationToken).ConfigureAwait(false);
118 }
119
120 span?.SetTag(AppsTelemetry.Tags.StateConversationDirty, convDirty);
121 span?.SetTag(AppsTelemetry.Tags.StateUserDirty, userDirty);
122 span?.SetTag(AppsTelemetry.Tags.StateBytesWritten, bytesWritten);
123 AppsTelemetry.StateBytesWritten.Record(bytesWritten);
124
125 if (convDirty || userDirty)
126 {
127 _logger.StateSaved(conversationId, convDirty, userDirty);
128 }
129 }
130 catch (Exception ex)
131 {
132 span?.RecordException(ex);
133 AppsTelemetry.StateCacheErrors.Add(1, new KeyValuePair<string, object?>(AppsTelemetry.Tags.Operation, "save"));
134 _logger.StateSaveFailed(ex, conversationId);
135 throw;
136 }
137 finally
138 {
139 AppsTelemetry.StateSaveDuration.Record(Stopwatch.GetElapsedTime(startTs).TotalMilliseconds);
140 }
141 }
142
143 /// <summary>
144 /// Removes conversation and/or user state from the cache.
145 /// </summary>
146 public async Task DeleteAsync(string conversationId, string? userId, CancellationToken cancellationToken)
147 {
148 using Activity? span = AppsTelemetry.Source.StartActivity(AppsTelemetry.Spans.StateDelete, ActivityKind.Internal);
149
150 try
151 {
152 string conversationKey = $"{_options.KeyPrefix}:conv:{conversationId}";
153 await _cache.RemoveAsync(conversationKey, cancellationToken).ConfigureAwait(false);
154
155 if (!string.IsNullOrEmpty(userId))
156 {
157 string userKey = $"{_options.KeyPrefix}:user:{conversationId}:{userId}";
158 await _cache.RemoveAsync(userKey, cancellationToken).ConfigureAwait(false);
159 }
160
161 _logger.StateDeleted(conversationId);
162 }
163 catch (Exception ex)
164 {
165 span?.RecordException(ex);
166 AppsTelemetry.StateCacheErrors.Add(1, new KeyValuePair<string, object?>(AppsTelemetry.Tags.Operation, "delete"));
167 throw;
168 }
169 }
170}
171