microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/msal-agentic-cache

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/src/Microsoft.Teams.Apps/Routing/Router.cs

230lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Diagnostics;
5using Microsoft.Extensions.Logging;
6using Microsoft.Teams.Apps.Diagnostics;
7using Microsoft.Teams.Apps.Handlers;
8using Microsoft.Teams.Apps.Schema;
9using Microsoft.Teams.Core.Diagnostics;
10
11namespace Microsoft.Teams.Apps.Routing;
12
13/// <summary>
14/// Router for dispatching Teams activities to registered routes
15/// </summary>
16internal sealed class Router
17{
18 private readonly List<RouteBase> _routes = [];
19 private readonly ILogger _logger;
20
21 internal Router(ILogger logger)
22 {
23 _logger = logger;
24 }
25
26 /// <summary>
27 /// Routes registered in the router.
28 /// </summary>
29 public IReadOnlyList<RouteBase> GetRoutes() => _routes.AsReadOnly();
30
31 /// <summary>
32 /// Registers a route. Routes are checked and invoked in registration order.
33 /// For non-invoke activities all matching routes run sequentially.
34 /// For invoke activities — routes must be non-overlapping.
35 /// </summary>
36 /// <exception cref="InvalidOperationException">
37 /// Thrown if a route with the same name is already registered, or if an invoke catch-all
38 /// is mixed with specific invoke handlers.
39 /// </exception>
40 public Router Register<TActivity>(Route<TActivity> route) where TActivity : TeamsActivity
41 {
42 if (_routes.Any(r => r.Name == route.Name))
43 {
44 throw new InvalidOperationException($"A route with name '{route.Name}' is already registered.");
45 }
46
47 string invokePrefix = TeamsActivityType.Invoke + "/";
48
49 if (route.Name == TeamsActivityType.Invoke && _routes.Any(r => r.Name.StartsWith(invokePrefix, StringComparison.Ordinal)))
50 {
51 throw new InvalidOperationException("Cannot register a catch-all invoke handler when specific invoke handlers are already registered. Use specific handlers or handle all invoke types inside OnInvoke.");
52 }
53
54 if (route.Name.StartsWith(invokePrefix, StringComparison.Ordinal) && _routes.Any(r => r.Name == TeamsActivityType.Invoke))
55 {
56 throw new InvalidOperationException($"Cannot register '{route.Name}' when a catch-all invoke handler is already registered. Remove OnInvoke or use specific handlers exclusively.");
57 }
58 _routes.Add(route);
59 _logger.LogDebug("Registered route '{Name}' for activity type '{ActivityType}'.", route.Name, typeof(TActivity).Name);
60 return this;
61 }
62
63 /// <summary>
64 /// Dispatches the activity to all matching routes in registration order.
65 /// </summary>
66 public async Task DispatchAsync(Context<TeamsActivity> ctx, CancellationToken cancellationToken = default)
67 {
68 ArgumentNullException.ThrowIfNull(ctx);
69
70 _logger.LogDebug("Routing activity of type '{Type}' against {RouteCount} registered routes.", ctx.Activity.Type, _routes.Count);
71
72 List<RouteBase> matchingRoutes = [];
73 foreach (RouteBase route in _routes)
74 {
75 bool matched = route.Matches(ctx.Activity);
76 _logger.LogTrace("Route '{Name}' selector returned {Result} for activity of type '{Type}'.", route.Name, matched, ctx.Activity.Type);
77 if (matched)
78 {
79 matchingRoutes.Add(route);
80 }
81 }
82
83 if (matchingRoutes.Count == 0 && _routes.Count > 0)
84 {
85 AppsTelemetry.HandlerUnmatched.Add(1, new KeyValuePair<string, object?>(AppsTelemetry.Tags.ActivityType, ctx.Activity.Type));
86 _logger.LogWarning(
87 "No routes matched activity of type '{Type}'.",
88 ctx.Activity.Type
89 );
90 return;
91 }
92
93 _logger.LogDebug("Matched {MatchCount} route(s) for activity of type '{Type}'.", matchingRoutes.Count, ctx.Activity.Type);
94
95 foreach (RouteBase route in matchingRoutes)
96 {
97 _logger.LogInformation("Dispatching '{Type}' activity to route '{Name}'.", ctx.Activity.Type, route.Name);
98 _logger.LogTrace("Dispatching activity to route '{Name}': {Activity}", route.Name, ctx.Activity.ToJson());
99
100 (string handlerType, string dispatch) = GetHandlerTags(route.Name);
101 TagList handlerTags = new()
102 {
103 { AppsTelemetry.Tags.HandlerType, handlerType },
104 { AppsTelemetry.Tags.HandlerDispatch, dispatch },
105 };
106
107 AppsTelemetry.HandlerDispatched.Add(1, handlerTags);
108
109 using Activity? span = AppsTelemetry.Source.StartActivity(AppsTelemetry.Spans.Handler, ActivityKind.Internal);
110 if (span is not null)
111 {
112 span.SetTag(AppsTelemetry.Tags.HandlerType, handlerType);
113 span.SetTag(AppsTelemetry.Tags.HandlerDispatch, dispatch);
114 }
115
116 long startTimestamp = Stopwatch.GetTimestamp();
117 try
118 {
119 await route.InvokeRoute(ctx, cancellationToken).ConfigureAwait(false);
120 }
121 catch (Exception ex)
122 {
123 AppsTelemetry.HandlerFailures.Add(1, handlerTags);
124 span.RecordException(ex);
125 throw;
126 }
127 finally
128 {
129 double elapsedMs = Stopwatch.GetElapsedTime(startTimestamp).TotalMilliseconds;
130 AppsTelemetry.HandlerDuration.Record(elapsedMs, handlerTags);
131 }
132
133 _logger.LogDebug("Completed route '{Name}' for '{Type}' activity.", route.Name, ctx.Activity.Type);
134 }
135 }
136
137 /// <summary>
138 /// Dispatches the specified activity context to the first matching route and returns the result of the invocation.
139 /// </summary>
140 /// <param name="ctx">The activity context to dispatch. Cannot be null.</param>
141 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
142 /// <returns>A task that represents the asynchronous operation. The task result contains a response object with the outcome
143 /// of the invocation.</returns>
144 public async Task<InvokeResponse> DispatchWithReturnAsync(Context<TeamsActivity> ctx, CancellationToken cancellationToken = default)
145 {
146 ArgumentNullException.ThrowIfNull(ctx);
147
148 string? name = ctx.Activity is InvokeActivity inv ? inv.Name : null;
149
150 _logger.LogDebug("Routing invoke activity with name '{Name}' against {RouteCount} registered routes.", name, _routes.Count);
151
152 List<RouteBase> matchingRoutes = [];
153 foreach (RouteBase route in _routes)
154 {
155 bool matched = route.Matches(ctx.Activity);
156 _logger.LogTrace("Route '{RouteName}' selector returned {Result} for invoke '{Name}'.", route.Name, matched, name);
157 if (matched)
158 {
159 matchingRoutes.Add(route);
160 }
161 }
162
163 if (matchingRoutes.Count == 0 && _routes.Count > 0)
164 {
165 TagList unmatchedTags = new()
166 {
167 { AppsTelemetry.Tags.ActivityType, ctx.Activity.Type },
168 { AppsTelemetry.Tags.InvokeName, name ?? string.Empty },
169 };
170 AppsTelemetry.HandlerUnmatched.Add(1, unmatchedTags);
171 _logger.LogWarning("No routes matched invoke activity with name '{Name}'; returning 501.", name);
172 return new InvokeResponse(501);
173 }
174
175 _logger.LogInformation("Dispatching invoke activity with name '{Name}' to route '{Route}'.", name, matchingRoutes[0].Name);
176 _logger.LogTrace("Dispatching invoke activity to route '{Route}': {Activity}", matchingRoutes[0].Name, ctx.Activity.ToJson());
177
178 (string handlerType, string dispatch) = GetHandlerTags(matchingRoutes[0].Name);
179 TagList handlerTags = new()
180 {
181 { AppsTelemetry.Tags.HandlerType, handlerType },
182 { AppsTelemetry.Tags.HandlerDispatch, dispatch },
183 };
184
185 AppsTelemetry.HandlerDispatched.Add(1, handlerTags);
186
187 using Activity? span = AppsTelemetry.Source.StartActivity(AppsTelemetry.Spans.Handler, ActivityKind.Internal);
188 if (span is not null)
189 {
190 span.SetTag(AppsTelemetry.Tags.HandlerType, handlerType);
191 span.SetTag(AppsTelemetry.Tags.HandlerDispatch, dispatch);
192 }
193
194 long startTimestamp = Stopwatch.GetTimestamp();
195 InvokeResponse response;
196 try
197 {
198 response = await matchingRoutes[0].InvokeRouteWithReturn(ctx, cancellationToken).ConfigureAwait(false);
199 }
200 catch (Exception ex)
201 {
202 AppsTelemetry.HandlerFailures.Add(1, handlerTags);
203 span.RecordException(ex);
204 throw;
205 }
206 finally
207 {
208 double elapsedMs = Stopwatch.GetElapsedTime(startTimestamp).TotalMilliseconds;
209 AppsTelemetry.HandlerDuration.Record(elapsedMs, handlerTags);
210 }
211
212 _logger.LogDebug("Completed invoke route '{Route}' for '{Name}' with status {Status}.", matchingRoutes[0].Name, name, response.Status);
213
214 return response;
215 }
216
217 private static (string handlerType, string dispatch) GetHandlerTags(string routeName)
218 {
219 const string invokePrefix = TeamsActivityType.Invoke + "/";
220 if (string.Equals(routeName, TeamsActivityType.Invoke, StringComparison.Ordinal))
221 {
222 return (routeName, "catchall");
223 }
224 if (routeName.StartsWith(invokePrefix, StringComparison.Ordinal))
225 {
226 return (routeName[invokePrefix.Length..], "invoke");
227 }
228 return (routeName, "type");
229 }
230}
231