microsoft/teams.net
Publicmirrored from https://github.com/microsoft/teams.netAvailable
core/src/Microsoft.Teams.Apps/State/TurnState.cs
111lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | using Microsoft.Teams.Core.Schema; |
| 5 | |
| 6 | namespace Microsoft.Teams.Apps.State; |
| 7 | |
| 8 | /// <summary> |
| 9 | /// Per-turn state container exposing the conversation and user scopes. It is loaded at the start of a |
| 10 | /// turn and saved when the turn completes successfully (see <see cref="TurnStateStore"/>). |
| 11 | /// </summary> |
| 12 | public sealed class TurnState |
| 13 | { |
| 14 | internal TurnState(StateScope conversation, StateScope user, string? conversationKey = null, string? userKey = null) |
| 15 | { |
| 16 | Conversation = conversation; |
| 17 | User = user; |
| 18 | ConversationKey = conversationKey; |
| 19 | UserKey = userKey; |
| 20 | } |
| 21 | |
| 22 | /// <summary>Per-conversation persisted scope.</summary> |
| 23 | public StateScope Conversation { get; } |
| 24 | |
| 25 | /// <summary>Per-user persisted scope.</summary> |
| 26 | public StateScope User { get; } |
| 27 | |
| 28 | /// <summary>Storage key for the conversation scope, or null when the scope is non-persisted this turn.</summary> |
| 29 | internal string? ConversationKey { get; } |
| 30 | |
| 31 | /// <summary>Storage key for the user scope, or null when the scope is non-persisted this turn.</summary> |
| 32 | internal string? UserKey { get; } |
| 33 | |
| 34 | /// <summary>True once the turn has completed and state has been saved; scope access then throws.</summary> |
| 35 | public bool IsCompleted { get; private set; } |
| 36 | |
| 37 | /// <summary>Gets a value by path: <c>"conversation.x"</c> or <c>"user.x"</c>.</summary> |
| 38 | /// <typeparam name="T">The value type to read.</typeparam> |
| 39 | /// <param name="path">The scoped path.</param> |
| 40 | public T? GetValue<T>(string path) |
| 41 | { |
| 42 | (StateScope scope, string key) = Resolve(path); |
| 43 | return scope.Get<T>(key); |
| 44 | } |
| 45 | |
| 46 | /// <summary>Sets a value by path: <c>"conversation.x"</c> or <c>"user.x"</c>.</summary> |
| 47 | /// <typeparam name="T">The value type to store.</typeparam> |
| 48 | /// <param name="path">The scoped path.</param> |
| 49 | /// <param name="value">The value to store.</param> |
| 50 | public void SetValue<T>(string path, T value) |
| 51 | { |
| 52 | (StateScope scope, string key) = Resolve(path); |
| 53 | scope.Set(key, value); |
| 54 | } |
| 55 | |
| 56 | /// <summary> |
| 57 | /// Derives the storage keys for the conversation and user scopes from an inbound |
| 58 | /// <see cref="CoreActivity"/>. Including the channel id in both keys prevents state from leaking |
| 59 | /// across channels/tenants. Either key is <see langword="null"/> when the activity lacks the parts |
| 60 | /// needed to build it; that scope is then non-persisted for the turn. |
| 61 | /// </summary> |
| 62 | /// <param name="activity">The incoming activity.</param> |
| 63 | internal static (string? ConversationKey, string? UserKey) DeriveKeys(CoreActivity activity) |
| 64 | { |
| 65 | string? channelId = activity.ChannelId; |
| 66 | string? conversationId = activity.Conversation?.Id; |
| 67 | string? fromId = activity.From?.Id; |
| 68 | |
| 69 | string? conversationKey = string.IsNullOrEmpty(channelId) || string.IsNullOrEmpty(conversationId) |
| 70 | ? null |
| 71 | : $"{channelId}/conversations/{conversationId}"; |
| 72 | |
| 73 | string? userKey = string.IsNullOrEmpty(channelId) || string.IsNullOrEmpty(fromId) |
| 74 | ? null |
| 75 | : $"{channelId}/users/{fromId}"; |
| 76 | |
| 77 | return (conversationKey, userKey); |
| 78 | } |
| 79 | |
| 80 | internal void Complete() |
| 81 | { |
| 82 | IsCompleted = true; |
| 83 | Conversation.Complete(); |
| 84 | User.Complete(); |
| 85 | } |
| 86 | |
| 87 | private (StateScope Scope, string Key) Resolve(string path) |
| 88 | { |
| 89 | ArgumentException.ThrowIfNullOrEmpty(path); |
| 90 | |
| 91 | int dot = path.IndexOf('.', StringComparison.Ordinal); |
| 92 | if (dot < 0) |
| 93 | { |
| 94 | throw new ArgumentException( |
| 95 | $"State path '{path}' must be scope-qualified, e.g. 'conversation.{path}' or 'user.{path}'.", |
| 96 | nameof(path)); |
| 97 | } |
| 98 | |
| 99 | string scopeName = path[..dot]; |
| 100 | string key = path[(dot + 1)..]; |
| 101 | StateScope scope = scopeName switch |
| 102 | { |
| 103 | "conversation" => Conversation, |
| 104 | "user" => User, |
| 105 | _ => throw new ArgumentException( |
| 106 | $"Unknown state scope '{scopeName}' in path '{path}'. Expected 'conversation' or 'user'.", |
| 107 | nameof(path)), |
| 108 | }; |
| 109 | return (scope, key); |
| 110 | } |
| 111 | } |
| 112 | |