< Summary

Information
Class: IceRpc.Router
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Router.cs
Tag: 275_13775359185
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>
 30437    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;
 8845    private readonly Dictionary<string, IDispatcher> _exactMatchRoutes = new();
 46
 8847    private readonly Stack<Func<IDispatcher, IDispatcher>> _middlewareStack = new();
 48
 8849    private readonly Dictionary<string, IDispatcher> _prefixMatchRoutes = new();
 50
 51    /// <summary>Constructs a top-level router.</summary>
 17652    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)
 3259        : this()
 3260    {
 3261        ServiceAddress.CheckPath(absolutePrefix);
 3062        absolutePrefix = NormalizePrefix(absolutePrefix);
 3063        AbsolutePrefix = absolutePrefix.Length > 1 ? absolutePrefix : "";
 3064    }
 65
 66    /// <inheritdoc/>
 67    public ValueTask<OutgoingResponse> DispatchAsync(
 68        IncomingRequest request,
 69        CancellationToken cancellationToken = default) =>
 14470        _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)
 3583    {
 3584        if (_dispatcher.IsValueCreated)
 285        {
 286            throw new InvalidOperationException(
 287                $"Cannot call {nameof(Map)} after calling {nameof(IDispatcher.DispatchAsync)}.");
 88        }
 3389        ServiceAddress.CheckPath(path);
 3390        _exactMatchRoutes[path] = dispatcher;
 3391        return this;
 3392    }
 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)
 47105    {
 47106        if (_dispatcher.IsValueCreated)
 2107        {
 2108            throw new InvalidOperationException(
 2109                $"Cannot call {nameof(Mount)} after calling {nameof(IDispatcher.DispatchAsync)}.");
 110        }
 45111        ServiceAddress.CheckPath(prefix);
 41112        prefix = NormalizePrefix(prefix);
 41113        _prefixMatchRoutes[prefix] = dispatcher;
 41114        return this;
 41115    }
 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)
 54124    {
 54125        if (_dispatcher.IsValueCreated)
 2126        {
 2127            throw new InvalidOperationException(
 2128                $"All middleware must be registered before calling {nameof(IDispatcher.DispatchAsync)}.");
 129        }
 52130        _middlewareStack.Push(middleware);
 52131        return this;
 52132    }
 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)
 134140    {
 134141        if (prefix.Length > 1)
 115142        {
 115143            prefix = prefix.TrimEnd('/');
 115144            if (prefix.Length == 0)
 4145            {
 4146                prefix = "/";
 4147            }
 115148        }
 134149        return prefix;
 134150    }
 151
 152    private IDispatcher CreateDispatchPipeline()
 68153    {
 68154        var exactMatchRoutes = _exactMatchRoutes.ToFrozenDictionary();
 68155        var prefixMatchRoutes = _prefixMatchRoutes.ToFrozenDictionary();
 156
 157        // The last dispatcher of the pipeline:
 68158        IDispatcher dispatchPipeline = new InlineDispatcher(
 68159            (request, cancellationToken) =>
 142160            {
 142161                string path = request.Path;
 68162
 142163                if (AbsolutePrefix.Length > 0)
 16164                {
 68165                    // Remove AbsolutePrefix from path. AbsolutePrefix starts with a '/' but usually does not end with
 68166                    // one.
 16167                    path = path.StartsWith(AbsolutePrefix, StringComparison.Ordinal) ?
 16168                        (path.Length == AbsolutePrefix.Length ? "/" : path[AbsolutePrefix.Length..]) :
 16169                        throw new InvalidOperationException(
 16170                            $"Received request for path '{path}' in router mounted at '{AbsolutePrefix}'.");
 16171                }
 68172                // else there is nothing to remove
 68173
 68174                // First check for an exact match
 142175                if (exactMatchRoutes.TryGetValue(path, out IDispatcher? dispatcher))
 109176                {
 109177                    return dispatcher.DispatchAsync(request, cancellationToken);
 68178                }
 68179                else
 33180                {
 68181                    // Then a prefix match
 33182                    string prefix = NormalizePrefix(path);
 68183
 194184                    foreach (int _ in Enumerable.Range(0, MaxSegments))
 64185                    {
 64186                        if (prefixMatchRoutes.TryGetValue(prefix, out dispatcher))
 31187                        {
 31188                            return dispatcher.DispatchAsync(request, cancellationToken);
 68189                        }
 68190
 33191                        if (prefix == "/")
 2192                        {
 2193                            return new(new OutgoingResponse(request, StatusCode.NotFound));
 68194                        }
 68195
 68196                        // Cut last segment
 31197                        int lastSlashPos = prefix.LastIndexOf('/');
 31198                        prefix = lastSlashPos > 0 ? NormalizePrefix(prefix[..lastSlashPos]) : "/";
 68199                        // and try again with the new shorter prefix
 31200                    }
 0201                    return new(new OutgoingResponse(request, StatusCode.InvalidData, "Too many segments in path."));
 68202                }
 210203            });
 204
 308205        foreach (Func<IDispatcher, IDispatcher> middleware in _middlewareStack)
 52206        {
 52207            dispatchPipeline = middleware(dispatchPipeline);
 52208        }
 68209        _middlewareStack.Clear(); // we no longer need these functions
 68210        return dispatchPipeline;
 68211    }
 212}