microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v2.0.8

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

142lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.Extensions.Logging;
5using Microsoft.Teams.Apps.Handlers;
6using Microsoft.Teams.Apps.Schema;
7
8namespace Microsoft.Teams.Apps.Routing;
9
10/// <summary>
11/// Router for dispatching Teams activities to registered routes
12/// </summary>
13internal sealed class Router
14{
15 private readonly List<RouteBase> _routes = [];
16 private readonly ILogger _logger;
17
18 internal Router(ILogger logger)
19 {
20 _logger = logger;
21 }
22
23 /// <summary>
24 /// Routes registered in the router.
25 /// </summary>
26 public IReadOnlyList<RouteBase> GetRoutes() => _routes.AsReadOnly();
27
28 /// <summary>
29 /// Registers a route. Routes are checked and invoked in registration order.
30 /// For non-invoke activities all matching routes run sequentially.
31 /// For invoke activities — routes must be non-overlapping.
32 /// </summary>
33 /// <exception cref="InvalidOperationException">
34 /// Thrown if a route with the same name is already registered, or if an invoke catch-all
35 /// is mixed with specific invoke handlers.
36 /// </exception>
37 public Router Register<TActivity>(Route<TActivity> route) where TActivity : TeamsActivity
38 {
39 if (_routes.Any(r => r.Name == route.Name))
40 {
41 throw new InvalidOperationException($"A route with name '{route.Name}' is already registered.");
42 }
43
44 string invokePrefix = TeamsActivityType.Invoke + "/";
45
46 if (route.Name == TeamsActivityType.Invoke && _routes.Any(r => r.Name.StartsWith(invokePrefix, StringComparison.Ordinal)))
47 {
48 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.");
49 }
50
51 if (route.Name.StartsWith(invokePrefix, StringComparison.Ordinal) && _routes.Any(r => r.Name == TeamsActivityType.Invoke))
52 {
53 throw new InvalidOperationException($"Cannot register '{route.Name}' when a catch-all invoke handler is already registered. Remove OnInvoke or use specific handlers exclusively.");
54 }
55 _routes.Add(route);
56 _logger.LogDebug("Registered route '{Name}' for activity type '{ActivityType}'.", route.Name, typeof(TActivity).Name);
57 return this;
58 }
59
60 /// <summary>
61 /// Dispatches the activity to all matching routes in registration order.
62 /// </summary>
63 public async Task DispatchAsync(Context<TeamsActivity> ctx, CancellationToken cancellationToken = default)
64 {
65 ArgumentNullException.ThrowIfNull(ctx);
66
67 _logger.LogDebug("Routing activity of type '{Type}' against {RouteCount} registered routes.", ctx.Activity.Type, _routes.Count);
68
69 List<RouteBase> matchingRoutes = [];
70 foreach (RouteBase route in _routes)
71 {
72 bool matched = route.Matches(ctx.Activity);
73 _logger.LogTrace("Route '{Name}' selector returned {Result} for activity of type '{Type}'.", route.Name, matched, ctx.Activity.Type);
74 if (matched)
75 {
76 matchingRoutes.Add(route);
77 }
78 }
79
80 if (matchingRoutes.Count == 0 && _routes.Count > 0)
81 {
82 _logger.LogWarning(
83 "No routes matched activity of type '{Type}'.",
84 ctx.Activity.Type
85 );
86 return;
87 }
88
89 _logger.LogDebug("Matched {MatchCount} route(s) for activity of type '{Type}'.", matchingRoutes.Count, ctx.Activity.Type);
90
91 foreach (RouteBase route in matchingRoutes)
92 {
93 _logger.LogInformation("Dispatching '{Type}' activity to route '{Name}'.", ctx.Activity.Type, route.Name);
94 _logger.LogTrace("Dispatching activity to route '{Name}': {Activity}", route.Name, ctx.Activity.ToJson());
95 await route.InvokeRoute(ctx, cancellationToken).ConfigureAwait(false);
96 _logger.LogDebug("Completed route '{Name}' for '{Type}' activity.", route.Name, ctx.Activity.Type);
97 }
98 }
99
100 /// <summary>
101 /// Dispatches the specified activity context to the first matching route and returns the result of the invocation.
102 /// </summary>
103 /// <param name="ctx">The activity context to dispatch. Cannot be null.</param>
104 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
105 /// <returns>A task that represents the asynchronous operation. The task result contains a response object with the outcome
106 /// of the invocation.</returns>
107 public async Task<InvokeResponse> DispatchWithReturnAsync(Context<TeamsActivity> ctx, CancellationToken cancellationToken = default)
108 {
109 ArgumentNullException.ThrowIfNull(ctx);
110
111 string? name = ctx.Activity is InvokeActivity inv ? inv.Name : null;
112
113 _logger.LogDebug("Routing invoke activity with name '{Name}' against {RouteCount} registered routes.", name, _routes.Count);
114
115 List<RouteBase> matchingRoutes = [];
116 foreach (RouteBase route in _routes)
117 {
118 bool matched = route.Matches(ctx.Activity);
119 _logger.LogTrace("Route '{RouteName}' selector returned {Result} for invoke '{Name}'.", route.Name, matched, name);
120 if (matched)
121 {
122 matchingRoutes.Add(route);
123 }
124 }
125
126 if (matchingRoutes.Count == 0 && _routes.Count > 0)
127 {
128 _logger.LogWarning("No routes matched invoke activity with name '{Name}'; returning 501.", name);
129 return new InvokeResponse(501);
130 }
131
132 _logger.LogInformation("Dispatching invoke activity with name '{Name}' to route '{Route}'.", name, matchingRoutes[0].Name);
133 _logger.LogTrace("Dispatching invoke activity to route '{Route}': {Activity}", matchingRoutes[0].Name, ctx.Activity.ToJson());
134
135 InvokeResponse response = await matchingRoutes[0].InvokeRouteWithReturn(ctx, cancellationToken).ConfigureAwait(false);
136
137 _logger.LogDebug("Completed invoke route '{Route}' for '{Name}' with status {Status}.", matchingRoutes[0].Name, name, response.Status);
138
139 return response;
140 }
141
142}
143