microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
samples/migration-bot

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

109lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.Extensions.Logging;
5using Microsoft.Teams.Bot.Apps.Handlers;
6using Microsoft.Teams.Bot.Apps.Schema;
7
8namespace Microsoft.Teams.Bot.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 return this;
57 }
58
59 /// <summary>
60 /// Dispatches the activity to all matching routes in registration order.
61 /// </summary>
62 public async Task DispatchAsync(Context<TeamsActivity> ctx, CancellationToken cancellationToken = default)
63 {
64 ArgumentNullException.ThrowIfNull(ctx);
65
66 List<RouteBase> matchingRoutes = [.. _routes.Where(r => r.Matches(ctx.Activity))];
67
68 if (matchingRoutes.Count == 0 && _routes.Count > 0)
69 {
70 _logger.LogWarning(
71 "No routes matched activity of type '{Type}'",
72 ctx.Activity.Type
73 );
74 return;
75 }
76
77 foreach (RouteBase route in matchingRoutes)
78 {
79 _logger.LogDebug("Dispatching '{Type}' activity to route '{Name}'.", ctx.Activity.Type, route.Name);
80 await route.InvokeRoute(ctx, cancellationToken).ConfigureAwait(false);
81 }
82 }
83
84 /// <summary>
85 /// Dispatches the specified activity context to the first matching route and returns the result of the invocation.
86 /// </summary>
87 /// <param name="ctx">The activity context to dispatch. Cannot be null.</param>
88 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
89 /// <returns>A task that represents the asynchronous operation. The task result contains a response object with the outcome
90 /// of the invocation.</returns>
91 public async Task<InvokeResponse> DispatchWithReturnAsync(Context<TeamsActivity> ctx, CancellationToken cancellationToken = default)
92 {
93 ArgumentNullException.ThrowIfNull(ctx);
94
95 List<RouteBase> matchingRoutes = [.. _routes.Where(r => r.Matches(ctx.Activity))];
96 string? name = ctx.Activity is InvokeActivity inv ? inv.Name : null;
97
98 if (matchingRoutes.Count == 0 && _routes.Count > 0)
99 {
100 _logger.LogWarning("No routes matched invoke activity with name '{Name}'; returning 501.", name);
101 return new InvokeResponse(501);
102 }
103
104 _logger.LogDebug("Dispatching invoke activity with name '{Name}' to route '{Route}'", name, matchingRoutes[0].Name);
105
106 return await matchingRoutes[0].InvokeRouteWithReturn(ctx, cancellationToken).ConfigureAwait(false);
107 }
108
109}
110