< Summary

Information
Class: IceRpc.Router
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Router.cs
Tag: 592_20856082467
Line coverage
98%
Covered lines: 105
Uncovered lines: 2
Coverable lines: 107
Total lines: 212
Line coverage: 98.1%
Branch coverage
84%
Covered branches: 27
Total branches: 32
Branch coverage: 84.3%
Method coverage
90%
Covered methods: 9
Total methods: 10
Method coverage: 90%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_AbsolutePrefix()100%11100%
.ctor()100%11100%
.ctor(...)100%22100%
DispatchAsync(...)100%11100%
Map(...)100%22100%
Mount(...)100%22100%
Use(...)100%22100%
ToString()0%620%
NormalizePrefix(...)100%44100%
CreateDispatchPipeline()83.33%181898.21%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Router.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using System.Collections.Frozen;
 4
 5namespace IceRpc;
 6
 7/// <summary>Provides methods for routing incoming requests to dispatchers.</summary>
 8/// <example>
 9/// The following example shows how you would install a middleware, and map a service.
 10/// <code source="../../docfx/examples/IceRpc.Examples/RouterExamples.cs" region="CreatingAndUsingTheRouterWithMiddlewar
 11/// You can easily create your own middleware and add it to the router. The next example shows how you can create a
 12/// middleware using an <see cref="InlineDispatcher"/> and add it to the router with
 13/// <see cref="Use(Func{IDispatcher, IDispatcher})"/>.
 14/// <code source="../../docfx/examples/IceRpc.Examples/RouterExamples.cs" region="CreatingAndUsingTheRouterWithAnInlineD
 15/// </example>
 16/// <remarks><para>The <see cref="Router"/> class allows you to define a dispatch pipeline for customizing how incoming
 17/// requests are processed. You utilize the Router class for creating routes with various middleware, sub-routers,
 18/// and dispatchers.</para>
 19/// <para>Incoming requests flow through the dispatch pipeline. An incoming request is first processed by the router's
 20/// middleware and then routed to a target dispatcher based on its path. The target dispatcher returns an outgoing
 21/// response that goes through the dispatch pipeline in the opposite direction.</para>
 22/// <para>The routing algorithm determines how incoming requests are routed to dispatchers. The router first checks if
 23/// a dispatcher is registered with the request's path, which corresponds to dispatchers registered using
 24/// <see cref="Map(string, IDispatcher)"/>. If there isn't a dispatcher registered for the request's path, the router
 25/// looks for dispatchers registered with a matching prefix, which corresponds to dispatchers installed using
 26/// <see cref="Mount(string, IDispatcher)"/>. When searching for a matching prefix, the router starts with the request
 27/// path and successively tries chopping segments from the end of the path until either the path is exhausted or a
 28/// dispatcher matching the prefix is found. Finally, if the router cannot find any dispatcher, it returns an
 29/// <see cref="OutgoingResponse"/> with a <see cref="StatusCode.NotFound"/> status code.</para></remarks>
 30public sealed class Router : IDispatcher
 31{
 32    /// <summary>Gets the absolute path-prefix of this router. The absolute path of a service added to this
 33    /// Router is: <c>$"{AbsolutePrefix}{path}"</c> where <c>path</c> corresponds to the argument given to
 34    /// <see cref="Map(string, IDispatcher)" />.</summary>
 35    /// <value>The absolute prefix of this router. It is either an empty string or a string with two or more
 36    /// characters starting with a <c>/</c>.</value>
 20637    public string AbsolutePrefix { get; } = "";
 38
 39    // When searching in the prefixMatchRoutes, we search up to MaxSegments before giving up. This prevents a
 40    // a malicious client from sending a request with a huge number of segments (/a/a/a/a/a/a/a/a/a/a...) that
 41    // results in numerous unsuccessful lookups.
 42    private const int MaxSegments = 10;
 43
 44    private readonly Lazy<IDispatcher> _dispatcher;
 5245    private readonly Dictionary<string, IDispatcher> _exactMatchRoutes = new();
 46
 5247    private readonly Stack<Func<IDispatcher, IDispatcher>> _middlewareStack = new();
 48
 5249    private readonly Dictionary<string, IDispatcher> _prefixMatchRoutes = new();
 50
 51    /// <summary>Constructs a top-level router.</summary>
 10452    public Router() => _dispatcher = new Lazy<IDispatcher>(CreateDispatchPipeline);
 53
 54    /// <summary>Constructs a router with an absolute prefix.</summary>
 55    /// <param name="absolutePrefix">The absolute prefix of the new router. It must start with a <c>/</c>.</param>
 56    /// <exception cref="FormatException">Thrown if <paramref name="absolutePrefix" /> is not a valid path.
 57    /// </exception>
 58    public Router(string absolutePrefix)
 1659        : this()
 1660    {
 1661        ServiceAddress.CheckPath(absolutePrefix);
 1562        absolutePrefix = NormalizePrefix(absolutePrefix);
 1563        AbsolutePrefix = absolutePrefix.Length > 1 ? absolutePrefix : "";
 1564    }
 65
 66    /// <inheritdoc/>
 67    public ValueTask<OutgoingResponse> DispatchAsync(
 68        IncomingRequest request,
 69        CancellationToken cancellationToken = default) =>
 11870        _dispatcher.Value.DispatchAsync(request, cancellationToken);
 71
 72    /// <summary>Registers a route with a path. If there is an existing route at the same path, it is replaced.
 73    /// </summary>
 74    /// <param name="path">The path of this route. It must match exactly the path of the request. In particular, it
 75    /// must start with a <c>/</c>.</param>
 76    /// <param name="dispatcher">The target of this route. It is typically a service.</param>
 77    /// <returns>This router.</returns>
 78    /// <exception cref="FormatException">Thrown if <paramref name="path" /> is not a valid path.</exception>
 79    /// <exception cref="InvalidOperationException">Thrown if <see cref="IDispatcher.DispatchAsync" /> was already
 80    /// called on this router.</exception>
 81    /// <seealso cref="Mount" />
 82    public Router Map(string path, IDispatcher dispatcher)
 2583    {
 2584        if (_dispatcher.IsValueCreated)
 185        {
 186            throw new InvalidOperationException(
 187                $"Cannot call {nameof(Map)} after calling {nameof(IDispatcher.DispatchAsync)}.");
 88        }
 2489        ServiceAddress.CheckPath(path);
 2490        _exactMatchRoutes[path] = dispatcher;
 2491        return this;
 2492    }
 93
 94    /// <summary>Registers a route with a prefix. If there is an existing route at the same prefix, it is replaced.
 95    /// </summary>
 96    /// <param name="prefix">The prefix of this route. This prefix will be compared with the start of the path of
 97    /// the request.</param>
 98    /// <param name="dispatcher">The target of this route.</param>
 99    /// <returns>This router.</returns>
 100    /// <exception cref="FormatException">Thrown if <paramref name="prefix" /> is not a valid path.</exception>
 101    /// <exception cref="InvalidOperationException">Thrown if <see cref="IDispatcher.DispatchAsync" /> was already
 102    /// called on this router.</exception>
 103    /// <seealso cref="Map(string, IDispatcher)" />
 104    public Router Mount(string prefix, IDispatcher dispatcher)
 24105    {
 24106        if (_dispatcher.IsValueCreated)
 1107        {
 1108            throw new InvalidOperationException(
 1109                $"Cannot call {nameof(Mount)} after calling {nameof(IDispatcher.DispatchAsync)}.");
 110        }
 23111        ServiceAddress.CheckPath(prefix);
 21112        prefix = NormalizePrefix(prefix);
 21113        _prefixMatchRoutes[prefix] = dispatcher;
 21114        return this;
 21115    }
 116
 117    /// <summary>Installs a middleware in this router. A middleware must be installed before calling
 118    /// <see cref="IDispatcher.DispatchAsync" />.</summary>
 119    /// <param name="middleware">The middleware to install.</param>
 120    /// <returns>This router.</returns>
 121    /// <exception cref="InvalidOperationException">Thrown if <see cref="IDispatcher.DispatchAsync" /> was already
 122    /// called on this router.</exception>
 123    public Router Use(Func<IDispatcher, IDispatcher> middleware)
 37124    {
 37125        if (_dispatcher.IsValueCreated)
 1126        {
 1127            throw new InvalidOperationException(
 1128                $"All middleware must be registered before calling {nameof(IDispatcher.DispatchAsync)}.");
 129        }
 36130        _middlewareStack.Push(middleware);
 36131        return this;
 36132    }
 133
 134    /// <summary>Returns a string that represents this router.</summary>
 135    /// <returns>A string that represents this router.</returns>
 0136    public override string ToString() => AbsolutePrefix.Length > 0 ? $"router({AbsolutePrefix})" : "router";
 137
 138    // Trim trailing slashes but keep the leading slash.
 139    private static string NormalizePrefix(string prefix)
 68140    {
 68141        if (prefix.Length > 1)
 58142        {
 58143            prefix = prefix.TrimEnd('/');
 58144            if (prefix.Length == 0)
 2145            {
 2146                prefix = "/";
 2147            }
 58148        }
 68149        return prefix;
 68150    }
 151
 152    private IDispatcher CreateDispatchPipeline()
 42153    {
 42154        var exactMatchRoutes = _exactMatchRoutes.ToFrozenDictionary();
 42155        var prefixMatchRoutes = _prefixMatchRoutes.ToFrozenDictionary();
 156
 157        // The last dispatcher of the pipeline:
 42158        IDispatcher dispatchPipeline = new InlineDispatcher(
 42159            (request, cancellationToken) =>
 117160            {
 117161                string path = request.Path;
 42162
 117163                if (AbsolutePrefix.Length > 0)
 8164                {
 42165                    // Remove AbsolutePrefix from path. AbsolutePrefix starts with a '/' but usually does not end with
 42166                    // one.
 8167                    path = path.StartsWith(AbsolutePrefix, StringComparison.Ordinal) ?
 8168                        (path.Length == AbsolutePrefix.Length ? "/" : path[AbsolutePrefix.Length..]) :
 8169                        throw new InvalidOperationException(
 8170                            $"Received request for path '{path}' in router mounted at '{AbsolutePrefix}'.");
 8171                }
 42172                // else there is nothing to remove
 42173
 42174                // First check for an exact match
 117175                if (exactMatchRoutes.TryGetValue(path, out IDispatcher? dispatcher))
 100176                {
 100177                    return dispatcher.DispatchAsync(request, cancellationToken);
 42178                }
 42179                else
 17180                {
 42181                    // Then a prefix match
 17182                    string prefix = NormalizePrefix(path);
 42183
 100184                    foreach (int _ in Enumerable.Range(0, MaxSegments))
 33185                    {
 33186                        if (prefixMatchRoutes.TryGetValue(prefix, out dispatcher))
 16187                        {
 16188                            return dispatcher.DispatchAsync(request, cancellationToken);
 42189                        }
 42190
 17191                        if (prefix == "/")
 1192                        {
 1193                            return new(new OutgoingResponse(request, StatusCode.NotFound));
 42194                        }
 42195
 42196                        // Cut last segment
 16197                        int lastSlashPos = prefix.LastIndexOf('/');
 16198                        prefix = lastSlashPos > 0 ? NormalizePrefix(prefix[..lastSlashPos]) : "/";
 42199                        // and try again with the new shorter prefix
 16200                    }
 0201                    return new(new OutgoingResponse(request, StatusCode.InvalidData, "Too many segments in path."));
 42202                }
 159203            });
 204
 198205        foreach (Func<IDispatcher, IDispatcher> middleware in _middlewareStack)
 36206        {
 36207            dispatchPipeline = middleware(dispatchPipeline);
 36208        }
 42209        _middlewareStack.Clear(); // we no longer need these functions
 42210        return dispatchPipeline;
 42211    }
 212}