< Summary

Information
Class: IceRpc.Router
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Router.cs
Tag: 1321_24790053727
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
Fully covered methods: 8
Total methods: 10
Method coverage: 90%
Full method coverage: 80%

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>
 20837    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;
 5345    private readonly Dictionary<string, IDispatcher> _exactMatchRoutes = new();
 46
 5347    private readonly Stack<Func<IDispatcher, IDispatcher>> _middlewareStack = new();
 48
 5349    private readonly Dictionary<string, IDispatcher> _prefixMatchRoutes = new();
 50
 51    /// <summary>Constructs a top-level router.</summary>
 10652    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) =>
 11970        _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)
 2683    {
 2684        if (_dispatcher.IsValueCreated)
 185        {
 186            throw new InvalidOperationException(
 187                $"Cannot call {nameof(Map)} after calling {nameof(IDispatcher.DispatchAsync)}.");
 88        }
 2589        ServiceAddress.CheckPath(path);
 2590        _exactMatchRoutes[path] = dispatcher;
 2591        return this;
 2592    }
 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)
 38124    {
 38125        if (_dispatcher.IsValueCreated)
 1126        {
 1127            throw new InvalidOperationException(
 1128                $"All middleware must be registered before calling {nameof(IDispatcher.DispatchAsync)}.");
 129        }
 37130        _middlewareStack.Push(middleware);
 37131        return this;
 37132    }
 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()
 43153    {
 43154        var exactMatchRoutes = _exactMatchRoutes.ToFrozenDictionary();
 43155        var prefixMatchRoutes = _prefixMatchRoutes.ToFrozenDictionary();
 156
 157        // The last dispatcher of the pipeline:
 43158        IDispatcher dispatchPipeline = new InlineDispatcher(
 43159            (request, cancellationToken) =>
 118160            {
 118161                string path = request.Path;
 43162
 118163                if (AbsolutePrefix.Length > 0)
 8164                {
 43165                    // Remove AbsolutePrefix from path. AbsolutePrefix starts with a '/' but usually does not end with
 43166                    // 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                }
 43172                // else there is nothing to remove
 43173
 43174                // First check for an exact match
 118175                if (exactMatchRoutes.TryGetValue(path, out IDispatcher? dispatcher))
 101176                {
 101177                    return dispatcher.DispatchAsync(request, cancellationToken);
 43178                }
 43179                else
 17180                {
 43181                    // Then a prefix match
 17182                    string prefix = NormalizePrefix(path);
 43183
 100184                    foreach (int _ in Enumerable.Range(0, MaxSegments))
 33185                    {
 33186                        if (prefixMatchRoutes.TryGetValue(prefix, out dispatcher))
 16187                        {
 16188                            return dispatcher.DispatchAsync(request, cancellationToken);
 43189                        }
 43190
 17191                        if (prefix == "/")
 1192                        {
 1193                            return new(new OutgoingResponse(request, StatusCode.NotFound));
 43194                        }
 43195
 43196                        // Cut last segment
 16197                        int lastSlashPos = prefix.LastIndexOf('/');
 16198                        prefix = lastSlashPos > 0 ? NormalizePrefix(prefix[..lastSlashPos]) : "/";
 43199                        // and try again with the new shorter prefix
 16200                    }
 0201                    return new(new OutgoingResponse(request, StatusCode.InvalidData, "Too many segments in path."));
 43202                }
 161203            });
 204
 203205        foreach (Func<IDispatcher, IDispatcher> middleware in _middlewareStack)
 37206        {
 37207            dispatchPipeline = middleware(dispatchPipeline);
 37208        }
 43209        _middlewareStack.Clear(); // we no longer need these functions
 43210        return dispatchPipeline;
 43211    }
 212}