microsoft/teams.net
Publicmirrored fromhttps://github.com/microsoft/teams.netAvailable
core/src/Microsoft.Teams.Bot.Apps/Routing/Router.cs
109lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | using Microsoft.Extensions.Logging; |
| 5 | using Microsoft.Teams.Bot.Apps.Handlers; |
| 6 | using Microsoft.Teams.Bot.Apps.Schema; |
| 7 | |
| 8 | namespace Microsoft.Teams.Bot.Apps.Routing; |
| 9 | |
| 10 | /// <summary> |
| 11 | /// Router for dispatching Teams activities to registered routes |
| 12 | /// </summary> |
| 13 | internal 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 | |